Tutustu Reactin ref-takaisinkutsujen optimoinnin hienouksiin. Opi, miksi se suoritetaan kahdesti, miten estää se useCallback-hookilla ja hallitse monimutkaisten sovellusten suorituskyky.
Reactin Ref-takaisinkutsujen hallinta: lopullinen opas suorituskyvyn optimointiin
Nykyaikaisen web-kehityksen maailmassa suorituskyky ei ole vain ominaisuus, se on välttämättömyys. Reactia käyttäville kehittäjille nopeiden ja responsiivisten käyttöliittymien rakentaminen on ensisijainen tavoite. Vaikka Reactin virtuaalinen DOM ja sovitusalgoritmi hoitavat suuren osan raskaasta työstä, on olemassa tiettyjä malleja ja API-rajapintoja, joiden syvällinen ymmärtäminen on ratkaisevan tärkeää huippusuorituskyvyn saavuttamiseksi. Yksi tällainen alue on ref-viittausten hallinta, erityisesti usein väärinymmärretty takaisinkutsu-refien (callback refs) käyttäytyminen.
Ref-viittaukset tarjoavat tavan päästä käsiksi renderöintimetodissa luotuihin DOM-solmuihin tai React-elementteihin – ne ovat olennainen pakokeino tehtävissä, kuten fokuksen hallinnassa, animaatioiden käynnistämisessä tai kolmannen osapuolen DOM-kirjastojen integroinnissa. Vaikka useRef on vakiintunut standardiksi yksinkertaisissa tapauksissa funktionaalisissa komponenteissa, takaisinkutsu-refit tarjoavat tehokkaamman ja hienojakoisemman hallinnan siihen, milloin viittaus asetetaan ja poistetaan. Tähän tehokkuuteen liittyy kuitenkin hienovaraisuus: takaisinkutsu-ref voi suorittua useita kertoja komponentin elinkaaren aikana, mikä voi johtaa suorituskyvyn pullonkauloihin ja bugeihin, jos sitä ei käsitellä oikein.
Tämä kattava opas selvittää Reactin ref-takaisinkutsun mysteerit. Tulemme tutkimaan:
- Mitä takaisinkutsu-refit ovat ja miten ne eroavat muista ref-tyypeistä.
- Keskeinen syy, miksi takaisinkutsu-refejä kutsutaan kahdesti (kerran
null-arvolla ja kerran elementillä). - Inline-funktioiden käytön suorituskykyansat ref-takaisinkutsuissa.
- Lopullinen ratkaisu optimointiin
useCallback-hookin avulla. - Edistyneet mallit riippuvuuksien käsittelyyn ja integrointiin ulkoisten kirjastojen kanssa.
Tämän artikkelin loppuun mennessä sinulla on tiedot käyttää takaisinkutsu-refejä itsevarmasti, varmistaen, että React-sovelluksesi eivät ole vain vakaita vaan myös erittäin suorituskykyisiä.
Pikakertaus: Mitä ovat takaisinkutsu-refit?
Ennen kuin sukellamme optimointiin, kerrataan lyhyesti, mikä takaisinkutsu-ref on. Sen sijaan, että välittäisit useRef()- tai React.createRef()-funktiolla luodun ref-olion, välitätkin funktion ref-attribuutille. React suorittaa tämän funktion, kun komponentti liitetään (mount) ja irrotetaan (unmount).
React kutsuu ref-takaisinkutsufunktiota DOM-elementti argumenttinaan, kun komponentti liitetään, ja se kutsuu sitä null-argumentilla, kun komponentti irrotetaan. Tämä antaa sinulle tarkan hallinnan juuri niinä hetkinä, kun viittaus tulee saataville tai on tuhoutumassa.
Tässä on yksinkertainen esimerkki funktionaalisessa komponentissa:
import React, { useState } from 'react';
function TextInputWithFocusButton() {
let textInput = null;
const setTextInputRef = element => {
console.log('Ref-takaisinkutsu suoritettu arvolla:', element);
textInput = element;
};
const focusTextInput = () => {
// Kohdenna tekstinsyöttökenttä raa'alla DOM API:lla
if (textInput) textInput.focus();
};
return (
<div>
<input type="text" ref={setTextInputRef} />
<button onClick={focusTextInput}>
Kohdenna tekstinsyöttökenttä
</button>
</div>
);
}
Tässä esimerkissä setTextInputRef on meidän takaisinkutsu-refimme. Sitä kutsutaan <input>-elementillä, kun se renderöidään, mikä antaa meille mahdollisuuden tallentaa se ja myöhemmin käyttää sitä focus()-metodin kutsumiseen.
Ydinongelma: Miksi Ref-takaisinkutsut suoritetaan kahdesti?
Keskeinen käyttäytymismalli, joka usein hämmentää kehittäjiä, on takaisinkutsun kaksoissuoritus. Kun komponentti, jolla on takaisinkutsu-ref, renderöidään, takaisinkutsufunktiota kutsutaan tyypillisesti kahdesti peräkkäin:
- Ensimmäinen kutsu: argumenttina
null. - Toinen kutsu: argumenttina DOM-elementin instanssi.
Tämä ei ole bugi; se on React-tiimin tarkoituksellinen suunnitteluvalinta. Kutsu null-arvolla merkitsee, että edellinen ref (jos sellainen oli) irrotetaan. Tämä antaa sinulle ratkaisevan tärkeän mahdollisuuden suorittaa siivoustoimenpiteitä. Jos esimerkiksi liitit edellisessä renderöinnissä tapahtumankuuntelijan solmuun, null-kutsu on täydellinen hetki poistaa se ennen uuden solmun liittämistä.
Ongelma ei kuitenkaan ole tämä liittämis/irrottamissykli. Todellinen suorituskykyongelma syntyy, kun tämä kaksoissuoritus tapahtuu jokaisen uudelleenrenderöinnin yhteydessä, jopa silloin, kun komponentin tila päivittyy tavalla, joka ei liity mitenkään itse ref-viittaukseen.
Inline-funktioiden ansa
Tarkastellaan tätä näennäisen viatonta toteutusta funktionaalisessa komponentissa, joka renderöidään uudelleen:
import React, { useState } from 'react';
function FrequentUpdatesComponent() {
const [count, setCount] = useState(0);
return (
<div>
<h3>Laskuri: {count}</h3>
<button onClick={() => setCount(c => c + 1)}>Kasvata</button>
<div
ref={(node) => {
// Tämä on inline-funktio!
console.log('Ref-takaisinkutsu suoritettu arvolla:', node);
}}
>
Olen viitattu elementti.
</div>
</div>
);
}
Jos suoritat tämän koodin ja napsautat "Kasvata"-painiketta, näet konsolissasi seuraavan tulosteen jokaisella napsautuksella:
Ref-takaisinkutsu suoritettu arvolla: null
Ref-takaisinkutsu suoritettu arvolla: <div>...</div>
Miksi näin tapahtuu? Koska jokaisessa renderöinnissä luot uuden funktiomuuttujan ref-propsille: (node) => { ... }. Sovitusprosessinsa aikana React vertaa edellisen renderöinnin propseja nykyisiin. Se huomaa, että ref-props on muuttunut (vanhasta funktiomuuttujasta uuteen). Reactin sopimus on selvä: jos ref-takaisinkutsu muuttuu, sen on ensin tyhjennettävä vanha ref kutsumalla sitä null-arvolla ja sitten asetettava uusi kutsumalla sitä DOM-solmulla. Tämä käynnistää siivous/alustussyklin tarpeettomasti jokaisella renderöinnillä.
Yksinkertaiselle console.log-komennolle tämä on pieni suorituskykyisku. Mutta kuvittele, että takaisinkutsusi tekee jotain raskasta:
- Monimutkaisten tapahtumankuuntelijoiden (esim. `scroll`, `resize`) liittäminen ja irrottaminen.
- Raskaan kolmannen osapuolen kirjaston (kuten D3.js-kaavion tai karttakirjaston) alustaminen.
- DOM-mittausten suorittaminen, jotka aiheuttavat sivun asettelun uudelleenlaskentaa (layout reflows).
Tämän logiikan suorittaminen jokaisen tilapäivityksen yhteydessä voi heikentää sovelluksesi suorituskykyä vakavasti ja aiheuttaa hienovaraisia, vaikeasti jäljitettäviä bugeja.
Ratkaisu: Muistiin tallentaminen `useCallback`-hookilla
Ratkaisu tähän ongelmaan on varmistaa, että React saa täsmälleen saman funktiomuuttujan ref-takaisinkutsua varten uudelleenrenderöintien välillä, ellei me nimenomaisesti halua sen muuttuvan. Tämä on täydellinen käyttötapaus useCallback-hookille.
useCallback palauttaa muistiin tallennetun (memoized) version takaisinkutsufunktiosta. Tämä muistiin tallennettu versio muuttuu vain, jos jokin sen riippuvuustaulukon riippuvuuksista muuttuu. Antamalla tyhjän riippuvuustaulukon ([]) voimme luoda vakaan funktion, joka säilyy koko komponentin elinkaaren ajan.
Muokataan edellistä esimerkkiämme käyttämällä useCallback-hookia:
import React, { useState, useCallback } from 'react';
function OptimizedComponent() {
const [count, setCount] = useState(0);
// Luo vakaa takaisinkutsufunktio useCallback-hookilla
const myRefCallback = useCallback(node => {
// Tämä logiikka suoritetaan nyt vain, kun komponentti liitetään ja irrotetaan
console.log('Ref-takaisinkutsu suoritettu arvolla:', node);
if (node !== null) {
// Voit suorittaa alustuslogiikan täällä
console.log('Elementti on liitetty!');
}
}, []); // <-- Tyhjä riippuvuustaulukko tarkoittaa, että funktio luodaan vain kerran
return (
<div>
<h3>Laskuri: {count}</h3>
<button onClick={() => setCount(c => c + 1)}>Kasvata</button>
<div ref={myRefCallback}>
Olen viitattu elementti.
</div>
</div>
);
}
Nyt, kun suoritat tämän optimoidun version, näet konsolilokin yhteensä vain kaksi kertaa:
- Kerran, kun komponentti alun perin liitetään (
Ref-takaisinkutsu suoritettu arvolla: <div>...</div>). - Kerran, kun komponentti irrotetaan (
Ref-takaisinkutsu suoritettu arvolla: null).
"Kasvata"-painikkeen napsauttaminen ei enää käynnistä ref-takaisinkutsua. Olemme onnistuneesti estäneet tarpeettoman siivous/alustussykin jokaisella uudelleenrenderöinnillä. React näkee saman funktiomuuttujan ref-propsille seuraavissa renderöinneissä ja päättelee oikein, ettei muutosta tarvita.
Edistyneet skenaariot ja parhaat käytännöt
Vaikka tyhjä riippuvuustaulukko on yleinen, on olemassa skenaarioita, joissa ref-takaisinkutsun on reagoitava propsien tai tilan muutoksiin. Tässä useCallback-hookin riippuvuustaulukon voima todella pääsee oikeuksiinsa.
Riippuvuuksien käsittely takaisinkutsussa
Kuvittele, että sinun täytyy suorittaa ref-takaisinkutsussasi logiikkaa, joka riippuu jostakin tilasta tai propsista. Esimerkiksi `data-`-attribuutin asettaminen nykyisen teeman perusteella.
function ThemedComponent({ theme }) {
const [internalState, setInternalState] = useState(0);
const themedRefCallback = useCallback(node => {
if (node !== null) {
// Tämä takaisinkutsu riippuu nyt 'theme'-propsista
console.log(`Asetetaan teema-attribuutiksi: ${theme}`);
node.setAttribute('data-theme', theme);
}
}, [theme]); // <-- Lisää 'theme' riippuvuustaulukkoon
return (
<div>
<p>Nykyinen teema: {theme}</p>
<div ref={themedRefCallback}>Tämän elementin teema päivittyy.</div>
{/* ... kuvittele tähän painike, jolla muutetaan vanhemman teemaa ... */}
</div>
);
}
Tässä esimerkissä olemme lisänneet theme-muuttujan useCallback-hookin riippuvuustaulukkoon. Tämä tarkoittaa:
- Uusi
themedRefCallback-funktio luodaan vain, kuntheme-props muuttuu. - Kun
theme-props muuttuu, React havaitsee uuden funktiomuuttujan ja suorittaa ref-takaisinkutsun uudelleen (ensinnull-arvolla, sitten elementillä). - Tämä antaa meidän efektillemme – `data-theme`-attribuutin asettamiselle – mahdollisuuden suorittua uudelleen päivitetyllä
theme-arvolla.
Tämä on oikea ja tarkoitettu käyttäytymismalli. Kerromme Reactille nimenomaisesti, että sen tulee käynnistää ref-logiikka uudelleen, kun sen riippuvuudet muuttuvat, samalla estäen sen suorittamisen epäolennaisten tilapäivitysten yhteydessä.
Integrointi kolmannen osapuolen kirjastojen kanssa
Yksi tehokkaimmista käyttötapauksista takaisinkutsu-refeille on kolmannen osapuolen kirjastojen instanssien alustaminen ja tuhoaminen, kun niiden on kiinnityttävä DOM-solmuun. Tämä malli hyödyntää täydellisesti takaisinkutsun liittämis/irrottamis-luonnetta.
Tässä on vankka malli kaavio- tai karttakirjaston kaltaisen kirjaston hallintaan:
import React, { useRef, useCallback, useEffect } from 'react';
import SomeChartingLibrary from 'some-charting-library';
function ChartComponent({ data }) {
// Käytä refiä kirjaston instanssin säilyttämiseen, ei DOM-solmun
const chartInstance = useRef(null);
const chartContainerRef = useCallback(node => {
// Solmu on null, kun komponentti irrotetaan
if (node === null) {
if (chartInstance.current) {
console.log('Siivotaan kaavioinstanssia...');
chartInstance.current.destroy(); // Kirjaston siivousmetodi
chartInstance.current = null;
}
return;
}
// Solmu on olemassa, joten voimme alustaa kaaviomme
console.log('Alustetaan kaavioinstanssia...');
const chart = new SomeChartingLibrary(node, {
// Konfiguraatioasetukset
data: data,
});
chartInstance.current = chart;
}, [data]); // Luo kaavio uudelleen, jos data-props muuttuu
return <div className="chart-container" ref={chartContainerRef} style={{ height: '400px' }} />;
}
Tämä malli on poikkeuksellisen siisti ja kestävä:
- Alustus: Kun `div`-elementti liitetään, takaisinkutsu saa `node`-solmun. Se luo uuden instanssin kaaviokirjastosta ja tallentaa sen
chartInstance.current-muuttujaan. - Siivous: Kun komponentti irrotetaan (tai jos `data` muuttuu, mikä käynnistää uuden suorituksen), takaisinkutsua kutsutaan ensin
null-arvolla. Koodi tarkistaa, onko kaavioinstanssi olemassa, ja jos on, kutsuu sendestroy()-metodia, mikä estää muistivuodot. - Päivitykset: Sisällyttämällä `data`-muuttujan riippuvuustaulukkoon varmistamme, että jos kaavion dataa on muutettava perustavanlaatuisesti, koko kaavio tuhotaan siististi ja alustetaan uudelleen uudella datalla. Yksinkertaisempia datan päivityksiä varten kirjasto saattaa tarjota `update()`-metodin, joka voitaisiin käsitellä erillisessä
useEffect-hookissa.
Suorituskykyvertailu: Milloin optimoinnilla on *oikeasti* väliä?
On tärkeää lähestyä suorituskykyä pragmaattisella asenteella. Vaikka jokaisen ref-takaisinkutsun kääriminen useCallback-hookiin on hyvä tapa, todellinen suorituskykyvaikutus vaihtelee dramaattisesti sen mukaan, mitä työtä takaisinkutsun sisällä tehdään.
Vähäisen vaikutuksen skenaariot
Jos takaisinkutsusi tekee vain yksinkertaisen muuttujan sijoituksen, uuden funktion luomisen aiheuttama lisäkuorma jokaisella renderöinnillä on mitätön. Nykyaikaiset JavaScript-moottorit ovat uskomattoman nopeita funktion luomisessa ja roskienkeruussa.
Esimerkki: ref={(node) => (myRef.current = node)}
Tällaisissa tapauksissa, vaikka teknisesti vähemmän optimaalista, et todennäköisesti koskaan mittaa suorituskykyeroa todellisessa sovelluksessa. Älä lankea ennenaikaisen optimoinnin ansaan.
Merkittävän vaikutuksen skenaariot
Sinun tulisi aina käyttää useCallback-hookia, kun ref-takaisinkutsusi tekee jotain seuraavista:
- DOM-manipulaatio: Suora luokkien lisääminen tai poistaminen, attribuuttien asettaminen tai elementtien kokojen mittaaminen (mikä voi käynnistää asettelun uudelleenlaskennan).
- Tapahtumankuuntelijat:
addEventListener- jaremoveEventListener-metodien kutsuminen. Tämän suorittaminen jokaisella renderöinnillä on varma tapa aiheuttaa bugeja ja suorituskykyongelmia. - Kirjaston instansiointi: Kuten kaavioesimerkissä näytettiin, monimutkaisten olioiden alustaminen ja purkaminen on kallista.
- Verkkopyynnöt: API-kutsun tekeminen DOM-elementin olemassaolon perusteella.
- Ref-viittausten välittäminen muistiin tallennetuille lapsikomponenteille: Jos välität ref-takaisinkutsun propsina
React.memo-funktiolla kääritylle lapsikomponentille, epävakaa inline-funktio rikkoo muistiin tallennuksen ja aiheuttaa lapsen tarpeettoman uudelleenrenderöinnin.
Hyvä nyrkkisääntö: Jos ref-takaisinkutsusi sisältää enemmän kuin yhden yksinkertaisen sijoituksen, tallenna se muistiin useCallback-hookilla.
Yhteenveto: Ennustettavan ja suorituskykyisen koodin kirjoittaminen
Reactin ref-takaisinkutsu on tehokas työkalu, joka tarjoaa hienojakoisen hallinnan DOM-solmuihin ja komponentti-instansseihin. Sen elinkaaren ymmärtäminen – erityisesti tarkoituksellinen null-kutsu siivouksen aikana – on avain sen tehokkaaseen käyttöön.
Olemme oppineet, että yleinen anti-pattern, jossa inline-funktiota käytetään ref-propsille, johtaa tarpeettomiin ja mahdollisesti kalliisiin uudelleensuorituksiin jokaisella renderöinnillä. Ratkaisu on elegantti ja idiomaattinen React: vakauta takaisinkutsufunktio käyttämällä useCallback-hookia.
Hallitsemalla tämän mallin voit:
- Estää suorituskyvyn pullonkauloja: Vältä kalliita alustus- ja purkulogiikoita jokaisen tilanmuutoksen yhteydessä.
- Poistaa bugeja: Varmista, että tapahtumankuuntelijoita ja kirjasto-instansseja hallitaan siististi ilman kaksoiskappaleita tai muistivuotoja.
- Kirjoittaa ennustettavaa koodia: Luo komponentteja, joiden ref-logiikka käyttäytyy täsmälleen odotetusti, suorittuen vain kun komponentti liitetään, irrotetaan tai kun sen tietyt riippuvuudet muuttuvat.
Seuraavan kerran kun tartut ref-viittaukseen ratkaistaksesi monimutkaisen ongelman, muista muistiin tallennetun takaisinkutsun voima. Se on pieni muutos koodissasi, joka voi tehdä merkittävän eron React-sovellustesi laadussa ja suorituskyvyssä, edistäen parempaa käyttäjäkokemusta kaikkialla maailmassa.