చాలా React పెర్ఫార్మెన్స్ ట్యుటోరియల్స్ ఒకే రకమైన తప్పుడు సలహాతో ముగుస్తాయి: ప్రతిదాన్ని useMemo మరియు useCallbackలో చుట్టి (wrap చేసి) పని ముగించేయండి అని. మీరు ఆ సలహాను పాటించి ఉంటే, బహుశా మీ అప్లికేషన్ను మరింత నెమ్మదిగా మార్చి ఉండవచ్చు. ఈ హుక్స్ (hooks) ఉచితం కాదు. ప్రతి ఒక్కటి మెమరీని కేటాయించడం, డిపెండెన్సీలను పోల్చడం మరియు క్యాష్ చేసిన విలువలను నిల్వ చేయడం వంటి పనులు చేస్తుంది. ఉద్దేశ్యం లేకుండా వీటిని ఉపయోగిస్తే, అవి ఆప్టిమైజేషన్కు బదులుగా ఓవర్హెడ్గా మారుతాయి.
అసలు విషయం ఏమిటో ఇప్పుడు తెలుసుకుందాం.
ప్రతి హుక్ నిజంగా ఏం చేస్తుంది
useMemo ఒక విలువను (value) గుర్తుంచుకుంటుంది. మీరు దానికి భారీ పని చేసే ఒక ఫంక్షన్ను ఇస్తే, అది ఫలితాన్ని తిరిగి ఇస్తుంది. తదుపరి రెండర్ (render) సమయంలో, మీ డిపెండెన్సీలు మారకపోతే, React ఆ గణనను (calculation) వదిలేసి పాత ఫలితాన్నే తిరిగి ఇస్తుంది.
useCallback ఒక ఫంక్షన్ను గుర్తుంచుకుంటుంది. ఇది మీ కోసం ఫంక్షన్ను రన్ చేయదు. దాని డిపెండెన్సీలు ఒకేలా ఉన్నంత వరకు, రెండర్ల మధ్య అదే ఫంక్షన్ ఇన్స్టెన్స్ను (instance) ఇది తిరిగి ఇస్తుంది.
ఇదే ప్రధాన వ్యత్యాసం. ఒకటి గణించబడిన విలువను (computed value) క్యాష్ చేస్తుంది. మరొకటి రిఫరెన్స్ను (reference) క్యాష్ చేస్తుంది. ఈ రెండింటినీ తప్పుగా వాడితే, కోడ్ ఆప్టిమైజ్ చేసినట్లు కనిపిస్తుంది కానీ, అదనపు మెమరీ ఖర్చు చేస్తూ, వాడకుండా ఉన్న కోడ్ లాగే పనిచేస్తుంది.
ఫంక్షన్ ఐడెంటిటీ (Function Identity) మీ ట్రీని ఎందుకు దెబ్బతీస్తుంది
ఒక కాంపోనెంట్ రీ-రెండర్ అయినప్పుడు, React మొత్తం ఫంక్షన్ బాడీని మళ్ళీ ఎగ్జిక్యూట్ చేస్తుంది. ప్రతి వేరియబుల్ మళ్ళీ సృష్టించబడుతుంది. ప్రతి ఇన్లైన్ ఫంక్షన్కు మెమరీలో కొత్త అడ్రస్ లభిస్తుంది.
JavaScriptలో, ఒకే విధమైన లాజిక్ ఉన్న రెండు ఫంక్షన్లు సమానం కావు. () => {} === () => {} అనేది false అని వస్తుంది. ఇదే నియమం ఆబ్జెక్ట్లు మరియు అర్రేలకు కూడా వర్తిస్తుంది. మీ పేరెంట్ కాంపోనెంట్ handleSubmitని నిర్వచించి దానిని చైల్డ్ కాంపోనెంట్కు పంపితే, ప్రతి రెండర్లో ఆ చైల్డ్ కొత్త ప్రాప్ను (prop) అందుకుంటుంది. చైల్డ్ React.memoతో చుట్టబడి ఉన్నప్పటికీ, కొత్త ఫంక్షన్ పాత ఫంక్షన్ లాగే పనిచేస్తుందని అది గుర్తించలేదు. రిఫరెన్స్ మారిపోయింది, కాబట్టి చైల్డ్ రీ-రెండర్ అవుతుంది.
useCallbackను పరిష్కరించడానికి రూపొందించిన మూల సమస్య ఇదే. ఇది వేగం గురించి కాదు, స్థిరత్వం (stability) గురించి.
useMemo ఎప్పుడు ఉపయోగపడాలి
మీరు చేసే పని గణనీయమైన ఖర్చుతో కూడుకున్నది (expensive) మరియు దాని వల్ల స్పష్టమైన ఆలస్యం (lag) కనిపిస్తున్నప్పుడు మీకు useMemo అవసరం అవుతుంది.
ఒక భారీ డేటాసెట్ను ఫిల్టర్ చేయడం గురించి ఆలోచించండి. మీకు వేల సంఖ్యలో రోస్ (rows) ఉన్న టేబుల్ మరియు సెర్చ్ ఇన్పుట్ ఉంటే, మీరు మీ కాంపోనెంట్ లోపల ఇలాంటి కోడ్ రాయవచ్చు:
const visibleRows = rows.filter(r => r.name.includes(query));
useMemo లేకుండా, ఆ లూప్ ప్రతి రెండర్లో నడుస్తుంది. యూజర్ సైడ్బార్ను టోగుల్ చేసే బటన్ను క్లిక్ చేస్తే, పేరెంట్ రీ-రెండర్ అవుతుంది, మరియు rows మరియు query మారకపోయినప్పటికీ మీ ఫిల్టర్ మళ్ళీ నడుస్తుంది. పెద్ద డేటాసెట్ ఉన్నప్పుడు, ఆ జాప్యం (stutter) స్పష్టంగా కనిపిస్తుంది.
useMemo ఫలితాన్ని స్థిరపరచడం (pinning) ద్వారా దీనిని సరిచేస్తుంది:
const visibleRows = useMemo(() => {
return rows.filter(r => r.name.includes(query));
}, [rows, query]);
ఇప్పుడు డిపెండెన్సీలు నిజంగా మారినప్పుడు మాత్రమే React ఆ ఫిల్టర్ను మళ్ళీ రన్ చేస్తుంది.
సంక్లిష్టమైన గణిత గణనలు (mathematical calculations), API రెస్పాన్స్లను చార్ట్-ఫ్రెండ్లీ ఫార్మాట్లుగా మార్చడం లేదా నిరంతరం రీ-కాల్క్యులేట్ చేయవలసి వచ్చే స్టేట్ను (state) పొందడం వంటి వాటికి కూడా ఇదే లాజిక్ వర్తిస్తుంది.
ఇక్కడ రెండవ, అంతగా కనిపించని ఒక ఉపయోగం కూడా ఉంది. మీరు ఒక ఆబ్జెక్ట్ లేదా అర్రేను లోకల్గా సృష్టించి, దానిని useEffect డిపెండెన్సీ అర్రేలో చేర్చినట్లయితే, ప్రతి రెండర్లో ఆ ఎఫెక్ట్ అనుకోకుండా ట్రిగ్గర్ అయ్యే అవకాశం ఉంది. ఇన్లైన్ ఆబ్జెక్ట్లు మరియు అర్రేలు ప్రతిసారీ కొత్త ఐడెంటిటీలను పొందుతాయి, కాబట్టి ఎఫెక్ట్ మారిన డిపెండెన్సీని చూసి మళ్ళీ రన్ అవుతుంది. ఆ ఆబ్జెక్ట్ను useMemoతో మెమోయిజ్ చేయడం వల్ల రిఫరెన్స్ స్థిరంగా ఉంటుంది మరియు అసలు డేటా మారినప్పుడు మాత్రమే మీ ఎఫెక్ట్ రన్ అవుతుంది.
useCallback ఎప్పుడు అవసరమవుతుంది
React.memoతో ఆప్టిమైజ్ చేయబడిన చైల్డ్ కాంపోనెంట్లకు మీరు హ్యాండ్లర్లను (handlers) పంపినప్పుడు useCallback చాలా ముఖ్యం.
ఒక కౌంటర్ను కలిగి ఉన్న పేరెంట్ కాంపోనెంట్ను ఊహించుకోండి. అది ఒక ఖరీదైన (expensive) చైల్డ్ లిస్ట్ను కూడా రెండర్ చేస్తుంది:
function Parent() {
const [count, setCount] = useState(0);
const handleItemClick = (id) => {
console.log(id);
};
return (
<div>
<button onClick={() => setCount(c + 1)}>{count}</button>
<ExpensiveList onItemClick={handleItemClick} />
</div>
);
}
ప్రతిసారి count మారినప్పుడు, Parent రీ-రెండర్ అవుతుంది. కొత్త handleItemClick సృష్టించబడుతుంది. ExpensiveList కొత్త ప్రాప్ రిఫరెన్స్ను అందుకుంటుంది కాబట్టి, అది కూడా రీ-రెండర్ అవుతుంది. ఒకవేళ ExpensiveList React.memoతో చుట్టబడి ఉంటే, ఫంక్షన్ ప్రాప్ మారిపోయినందున ఆ మెమోయిజేషన్ పూర్తిగా వృథా అవుతుంది.
useCallback రిఫరెన్స్ను కాపాడుతుంది:
const handleItemClick = useCallback((id) => {
console.log(id);
}, []);
ఇప్పుడు ExpensiveList నిజంగా అవసరమైనప్పుడు మాత్రమే రీ-రెండర్ అవుతుంది.
మరొక ముఖ్యమైన పరిస్థితి useEffectకి సంబంధించింది. ఒక ఎఫెక్ట్ మీ కాంపోనెంట్ లోపల నిర్వచించబడిన ఫంక్షన్కు సబ్స్క్రైబ్ అయితే, మరియు ఆ ఫంక్షన్ ప్రతి రెండర్లో ఐడెంటిటీని మారిస్తే, ఆ ఎఫెక్ట్ పదేపదే టేర్డౌన్ (teardown) మరియు రీ-సబ్స్క్రైబ్ అవుతుంది. ఫంక్షన్ను మెమోయిజ్ చేయడం వల్ల ఎఫెక్ట్ స్థిరంగా ఉంటుంది.
డిపెండెన్సీ అర్రే ట్రాప్ (Dependency Array Trap) మరియు స్టేల్ క్లోజర్స్ (Stale Closures)
రెండు హుక్స్ డిపెండెన్సీ అర్రేలపై ఆధారపడి ఉంటాయి, మరియు ఇక్కడే చాలా బగ్స్ దాగి ఉంటాయి.
మీరు dependency array నుండి ఒక వేరియబుల్ను వదిలేస్తే, మీ memoized function లేదా value ఆ వేరియబుల్ యొక్క పాత వెర్షన్ను తీసుకుంటుంది. దీనినే stale closure అంటారు. UI తాజా డేటాను చూపించవచ్చు, కానీ మీ callback ఇంకా మూడు renders క్రితం ఉన్న stateనే చూస్తూ ఉంటుంది. దీనికి పరిష్కారం చాలా సరళమైనది, కానీ code reviews సమయంలో దీనిని గమనించడం కష్టం: hook లోపల ఉపయోగించే, మారే అవకాశం ఉన్న ప్రతి విలువను తప్పనిసరిగా చేర్చండి.
react-hooks/exhaustive-deps ESLint ruleని రన్ చేయండి. ఇది స్పష్టమైన లోపాలను (omissions) గుర్తిస్తుంది. కానీ దీనిని ఒక రోబోట్లా చూడకండి. ప్రతి dependency ఎందుకు ముఖ్యమో అర్థం చేసుకోండి.
అతిగా ఆప్టిమైజ్ చేయడం వల్ల కలిగే దాగి ఉన్న నష్టం
బిగినర్స్ తరచుగా ప్రతి function మరియు ప్రతి value కి ఈ hooks ఉపయోగించి రక్షణ కల్పించడానికి ప్రయత్నిస్తారు, ఎందుకంటే అది సురక్షితంగా అనిపిస్తుంది. కానీ ఆ అలవాటు రివర్స్ అవుతుంది.
React తన cached valuesను మెమరీలో నిల్వ చేయాలి. ప్రతి render సమయంలో, అది మీ dependency array ద్వారా వెళ్ళి (iterate), ప్రతి ఐటెమ్ను Object.is ఉపయోగించి పోల్చాలి. ఆ పోలిక తక్కువ ఖర్చుతో కూడుకున్నదే కావచ్చు, కానీ అది ఉచితం కాదు. మీరు onClick={() => setOpen(true)} వంటి చిన్న event handlerని useCallback లోపల ఉంచితే, తక్షణమే సృష్టించగలిగే ఒక function కోసం మీరు అనవసరంగా memory మరియు CPU ఖర్చులను భరిస్తున్నారు.
ఈ hooks కోడ్లో అనవసరమైన గందరగోళాన్ని (noise) కూడా పెంచుతాయి. useMemo మరియు useCallback తో చుట్టబడిన కోడ్ను చదవడం మరియు నిర్వహించడం (maintain) కష్టం. ప్రతి dependency array అనేది మిమ్మల్ని ఇబ్బంది పెట్టడానికి సిద్ధంగా ఉన్న ఒక potential stale closure.
అసలైన సూత్రం (rule of thumb) అంత ఆకర్షణీయంగా లేకపోయినా చాలా ప్రభావవంతమైనది: మొదట సాధారణ కోడ్ను రాయండి. ఏదైనా సమస్య ఉందని నిరూపితమైనప్పుడు మాత్రమే ఆప్టిమైజ్ చేయండి. ఏ components ఖరీదైనవో (expensive) మరియు ఏ renders వృథా అవుతున్నాయో గుర్తించడానికి React DevTools Profilerని ఉపయోగించండి. ఒక render కొన్ని మిల్లీ సెకన్ల కంటే తక్కువ సమయం తీసుకుంటే, ఏ యూజర్ కూడా దానిని గమనించలేరు, మరియు మీ memoization వల్ల ఎటువంటి ప్రయోజనం ఉండదు.
ముగింపు (The Bottom Line)
useMemo అనేది ఖరీదైన (expensive) values కోసం. useCallback అనేది స్థిరమైన (stable) function references కోసం. ఈ రెండు hooks కూడా మీ componentను నేరుగా వేగంగా render చేయవు; అవి అనవసరమైన downstream పనులను నివారిస్తాయి. మొదట వీటిని వాడకుండా ప్రారంభించండి, అసలైన toolingతో కొలవండి, మరియు profiler ఎక్కడ bottleneck చూపుతుందో అక్కడ మాత్రమే వీటిని చేర్చండి. అప్పుడప్పుడు rerender అయ్యే క్లీన్ కోడ్, ప్రతిదీ memoize చేసే over-engineered కోడ్ కంటే ఎప్పుడూ మెరుగ్గా ఉంటుంది.
