మీ యూజర్ యొక్క బ్రౌజర్ ట్యాబ్ ముప్పై నిమిషాల తర్వాత ఫ్రీజ్ అవుతుంది. UI సరిగ్గా పనిచేయదు (stutters). ఆపై బ్రౌజర్ out-of-memory ఎర్రర్తో క్రాష్ అవుతుంది.
మీరు కాంపోనెంట్ కోడ్ను లైన్ బై లైన్ పరిశీలించినా ఏ తప్పూ కనిపించదు. React లో మెమరీ లీక్స్ వల్ల కలిగే చిరాకుకరమైన విషయం ఇదే. బగ్ మీ JSX సింటాక్స్లో లేదా మీ హుక్ లాజిక్లో ఉండదు. ఇది మీ కాంపోనెంట్ మరియు బ్రౌజర్ యొక్క గార్బేజ్ కలెక్టర్ మధ్య ఉన్న ఖాళీలో ఉంటుంది. ఒక అనామక ఈవెంట్ లిజనర్ (stray event listener) లేదా ఎక్కువ కాలం ఉండే క్లోజర్ (long-lived closure) కలెక్టర్ మెమరీని తిరిగి పొందకుండా అడ్డుకుంటుంది. మీరు ఒక కాంపోనెంట్ను అన్మౌంట్ చేసినప్పటికీ, ఒకే ఒక్క మిగిలిపోయిన రిఫరెన్స్ మొత్తం ట్రీని హీప్లో సజీవంగా ఉంచుతుంది. కోడ్ను కేవలం చదవడం ద్వారా మీరు ఈ లీక్లను కనుగొనలేరు. ఈ సమస్య ఉపరితలం కింద దాగి ఉంటుంది, కంటికి కనిపించదు మరియు యూనిట్ టెస్ట్లలో కూడా బయటపడదు.
మెమరీ లీక్స్ ఎందుకు స్పష్టంగా కనిపిస్తూ కూడా దాగి ఉంటాయి
V8 వంటి JavaScript ఇంజన్లు మెమరీని ఆటోమేటిక్గా నిర్వహిస్తాయి. ఒక ఆబ్జెక్ట్ నుండి రూట్ వరకు ఎటువంటి రిఫరెన్స్ పాత్ లేనప్పుడు, ఇంజన్ ఆ ఆబ్జెక్ట్ను గార్బేజ్గా గుర్తించి ఆ స్థలాన్ని తిరిగి పొందుతుంది. మీరు అనుకున్న దానికంటే ఎక్కువ కాలం ఒక హిడెన్ రిఫరెన్స్ మిగిలి ఉన్నంత వరకు ఈ ప్రక్రియ బాగానే పనిచేస్తుంది.
React లో, ప్రమాదం తరచుగా కాంపోనెంట్లు మరియు DOM మధ్య ఉన్న సరిహద్దు వద్ద కనిపిస్తుంది. మీరు ఒక మోడల్ లోపల window కి resize లిజనర్ను అటాచ్ చేయవచ్చు, లేదా డాష్బోర్డ్ విడ్జెట్లో WebSocket కి సబ్స్క్రైబ్ చేయవచ్చు. యూజర్ మోడల్ను మూసివేసినా లేదా వేరే పేజీకి వెళ్లినా, కాంపోనెంట్ అన్మౌంట్ అవుతుంది. ఒకవేళ సబ్స్క్రిప్షన్ మిగిలిపోతే, ఇంజన్ గ్లోబల్ window ఆబ్జెక్ట్ నుండి మీ హ్యాండ్లర్కు, మరియు మీ హ్యాండ్లర్ నుండి కాంపోనెంట్ క్లోజర్కు ఒక చెల్లుబాటు అయ్యే రిఫరెన్స్ను చూస్తుంది. కాంపోనెంట్, దాని props, దాని state మరియు దాని మొత్తం DOM నోడ్స్ సబ్ట్రీ మెమరీలో అలాగే ఉండిపోతాయి. వందలాది ఇంటరాక్షన్ల తర్వాత, ఈ పిన్ చేయబడిన ఆబ్జెక్ట్లు పేరుకుపోతాయి. మెమరీ వినియోగం ఎప్పుడూ పూర్తిగా తగ్గని sawtooth pattern లో పెరుగుతూ ఉంటుంది.
ఎంటర్ప్రైజ్ డాష్బోర్డ్ సమస్య
యూజర్లు గంటల తరబడి ఒకే పేజీలో ఉండే ఎంటర్ప్రైజ్ డాష్బోర్డ్లలో ఇది ఎక్కువగా జరుగుతుంది. మానిటరింగ్ ప్యానెల్స్, అనలిటిక్స్ వ్యూస్ లేదా టికెటింగ్ సిస్టమ్లను తీసుకోండి. ఒక యూజర్ డీటెయిల్ మోడల్ను ఓపెన్ చేస్తారు, పెద్ద డేటాసెట్ను ఫిల్టర్ చేస్తారు లేదా సింగిల్-పేజీ యాప్లో ట్యాబ్లను మారుస్తారు. ప్రతి ఇంటరాక్షన్ విడిగా చూస్తే బాగున్నట్లు అనిపిస్తుంది. కానీ కాలక్రమేణా, ఒంటరిగా మిగిలిపోయిన (orphaned) నోడ్స్ మరియు డిటాచ్డ్ లిజనర్లు పేరుకుపోతాయి. అప్లికేషన్ నెమ్మదించడానికి కారణం ఏదైనా ఒక భారీ రెండర్ కాదు, కానీ హీప్ పెరిగిపోయి తరచుగా మరియు ఖరీదైన గార్బేజ్ కలెక్షన్ పాజ్లను (garbage collection pauses) ప్రేరేపించడం వల్ల అవుతుంది.
మీ useEffect హుక్స్ గురించి ఊహించడం ఆపండి. లీక్ ఉందో లేదో తెలుసుకోవడానికి ఏకైక మార్గం హీప్ను నేరుగా కొలవడం. Chrome DevTools మీకు ఆ వీక్షణను ఇస్తుంది.
Chrome DevTools తో లీక్లను వేటాడటం
మీకు ఒక పునరావృతమయ్యే క్రమం (reproducible sequence) మరియు కొన్ని నిమిషాల ఏకాగ్రత అవసరం. మీ అప్లికేషన్ను Chromeలో ఓపెన్ చేయండి, DevTools ప్రారంభించి, Memory ట్యాబ్కు వెళ్లండి.
బేస్లైన్ను రికార్డ్ చేయండి. Heap snapshotని ఎంచుకుని Take snapshot క్లిక్ చేయండి. ఇది ప్రస్తుతం JavaScript హీప్లో ఉన్న ప్రతి ఆబ్జెక్ట్ను క్యాప్చర్ చేస్తుంది మరియు ప్రారంభ మెమరీని చూపిస్తుంది. పేజీ దాని ప్రారంభ ఐడిల్ స్టేట్లోకి వచ్చిన తర్వాత మాత్రమే ఇది చేయండి, ప్రారంభ లోడింగ్ సమయంలో కాదు, తద్వారా మీరు కేవలం యూజర్ చర్యల వల్ల కలిగే పెరుగుదలను మాత్రమే కొలుస్తారు.
యాక్షన్ను ట్రిగ్గర్ చేయండి. లీక్కు కారణమవుతుందని మీరు అనుమానించే ఖచ్చితమైన UI ఇంటరాక్షన్ను చేయండి. ఒక మోడల్ను ఓపెన్ చేసి మూసివేయండి, ఒక కాంప్లెక్స్ చార్ట్ను టోగుల్ చేయండి లేదా రూట్ను మార్చి తిరిగి వెళ్లండి. పూర్తయిన తర్వాత, యాప్ను దాని అసలు విజువల్ స్టేట్కు తీసుకురండి. ఈ దశ చాలా కీలకం. బేస్లైన్ సమయంలో UI ఎలా ఉందో, అలాగే ఉండాలని మీరు కోరుకుంటారు. UI ఖాళీగా ఉన్నట్లు అనిపించినప్పటికీ హీప్ పెరిగితే, మీకు లీక్ ఉన్నట్లు బలమైన ఆధారాలు ఉన్నట్లు లెక్క.
గార్బేజ్ కలెక్షన్ను ఫోర్స్ చేయండి. Memory ట్యాబ్లోని చెత్తబుట్ట (trash can) ఐకాన్ను క్లిక్ చేయండి. ఇది పూర్తి GC సైకిల్ను ప్రేరేపిస్తుంది మరియు తదుపరి కలెక్షన్ వరకు చట్టబద్ధంగా మిగిలిపోయిన తాత్కాలిక ఆబ్జెక్ట్లను క్లియర్ చేస్తుంది. మిగిలి ఉన్నవి నిజమైన లీక్లు, అంటే సేకరించబడాల్సిన ఆబ్జెక్ట్లు కానీ పొరపాటున జరిగిన రిఫరెన్స్ల వల్ల సజీవంగా ఉన్నవి.
రెండవ స్నాప్షాట్ను తీసుకోండి. మళ్ళీ Take snapshot క్లిక్ చేయండి. ఇప్పుడు మీ వద్ద ఒకే రకమైన UI పరిస్థితులలో తీసిన హీప్ యొక్క రెండు ఫోటోలు ఉన్నాయి.
ఫలితాలను పోల్చండి. వ్యూను Summary నుండి Comparison కి మార్చండి. బేస్లైన్ను మీ మొదటి స్నాప్షాట్గా మరియు పోల్చాల్సిన స్నాప్షాట్ను రెండవ దానిగా సెట్ చేయండి. Comparison వ్యూ ప్రతి ఆబ్జెక్ట్ కేటగిరీని జాబితా చేస్తుంది మరియు రెండు క్యాప్చర్ల మధ్య ఆబ్జెక్ట్ కౌంట్లలో జరిగిన నికర మార్పును (delta) చూపిస్తుంది.
డెల్టా (Delta) ద్వారా సార్ట్ చేయండి. గణనీయంగా పెరిగిన కేటగిరీల కోసం చూడండి. ముఖ్యంగా Detached HTMLElement మరియు React fiber నోడ్లపై దృష్టి పెట్టండి. ఒక డిటాచ్డ్ HTML ఎలిమెంట్ అంటే యాక్టివ్ డాక్యుమెంట్ ట్రీకి ఇకపై అనుసంధానించబడలేదు, కానీ ఏదో ఒక JavaScript రిఫరెన్స్ దానిని ఇంకా పట్టుకుని ఉంది. ఇవి లీక్ యొక్క స్పష్టమైన ఆధారాలు (smoking guns). మీరు మోడల్ను మూసివేసినా లేదా కాంపోనెంట్ను అన్మౌంట్ చేసినా ఇవి ఉండకూడదు.
Retaining Path ను చదవడం
మీరు స్నాప్షాట్లో ఒక డిటాచ్డ్ ఎలిమెంట్ను ఎంచుకున్నప్పుడు, Chrome దిగువ ప్యానెల్లో రిటైనింగ్ పాత్ (retaining path) ను చూపుతుంది. ఈ పాత్ అనేది రూట్ నుండి ఎంచుకున్న ఆబ్జెక్ట్ వరకు ఉన్న రిఫరెన్స్ల గొలుసు. దీనిని జాగ్రత్తగా అనుసరించండి. మీరు తరచుగా ఒక ఈవెంట్ లిజనర్ (event listener), ఒక IntersectionObserver, ఒక setInterval ID, లేదా మీ కాంపోనెంట్లోని ఒక నిర్దిష్ట లైన్ను సూచించే క్లోజర్ (closure) ను కనుగొంటారు.
మీకు తెలిసిన పేర్ల కోసం వెతకండి. మీ కోడ్బేస్ నుండి ఒక ఫంక్షన్ పేరుతో windowకి అటాచ్ చేయబడిన లిజనర్ను మీరు చూస్తే, మీరు ఆ యాంకర్ (anchor) ను కనుగొన్నట్లే. ఆ లిజనర్ను కలిగి ఉన్న ఆబ్జెక్ట్ మీ పూర్తి కాంపోనెంట్ను లైవ్లో ఉంచుతుంది. కొన్నిసార్లు ఈ గొలుసు థర్డ్-పార్టీ లైబ్రరీ ద్వారా వెళ్తుంది. అటువంటి సందర్భాలలో, మీరు క్లీనప్ ఫంక్షన్లో పిలవmayı మర్చిపోయిన ఎక్స్ప్లిసిట్ టేర్డౌన్ కాల్ (explicit teardown call) ఆ లైబ్రరీకి అవసరమా అనేది తనిఖీ చేయండి.
మూల కారణాలను పరిష్కరించడం
మీరు రిటైనింగ్ పాత్ను గుర్తించిన తర్వాత, పరిష్కారం సాధారణంగా మెకానికల్ (mechanical) గా ఉంటుంది కానీ టీమ్ అంతటా క్రమశిక్షణ అవసరం.
క్లీనప్ ఫంక్షన్లను ఉపయోగించండి. మీరు window లేదా documentకి లిజనర్లను జోడించినప్పుడు, useEffectలో ఎల్లప్పుడూ ఒక క్లీనప్ ఫంక్షన్ను రిటర్న్ చేయండి. మీ ఎఫెక్ట్ ఒక resize ఈవెంట్కు సబ్స్క్రైబ్ అయితే, కాంపోనెంట్ అన్మౌంట్ (unmount) కావడానికి ముందే ఆ సబ్స్క్రిప్షన్ను తొలగించండి. React కాంపోనెంట్ను తొలగించినప్పుడు క్లీనప్ రన్ అవుతుంది, ఇది బాహ్య కనెక్షన్లను తెంపివేయడానికి మీకు ఒక గ్యారెంటీడ్ హుక్ను అందిస్తుంది.
రిఫరెన్స్లను స్థిరీకరించండి. మీ హ్యాండ్లర్లను useCallbackలో చుట్టండి. దీనివల్ల మీరు మొదట addEventListenerకి పంపిన అదే ఫంక్షన్ రిఫరెన్స్ను removeEventListenerకి పంపుతున్నారని నిర్ధారించుకోవచ్చు. మీరు window.addEventListener('resize', () => { ... }) వంటి ఇన్లైన్ ఫంక్షన్ను రిజిస్టర్ చేసి, తర్వాత మరొక ఇన్లైన్ ఫంక్షన్తో దానిని తొలగించడానికి ప్రయత్నిస్తే, రిఫరెన్స్లు సరిపోలవు. లిజనర్ అటాచ్ అయి ఉంటుంది. దానిలోని క్లోజర్ మీ కాంపోనెంట్ స్టేట్ను లైవ్లో ఉంచుతుంది. స్టేబుల్ డిపెండెన్సీ అర్రేతో కూడిన useCallback ఈ ఐడెంటిటీ మిస్మ్యాచ్ (identity mismatch) ను నివారిస్తుంది.
గ్లోబల్ లింక్లను తెంపివేయండి. బ్రౌజర్ యొక్క ఈవెంట్ టార్గెట్ ఆబ్జెక్ట్లు, అంటే window మరియు document, పేజీ యొక్క జీవితకాలం వరకు ఉంటాయి అని గుర్తుంచుకోండి. వాటి నుండి మీ కాంపోనెంట్లోకి వచ్చే ఏ రిఫరెన్స్ అయినా గ్లోబల్ యాంకర్ (global anchor) లాగా పనిచేస్తుంది. లిజనర్ను తొలగించడం వల్ల ఆ యాంకర్ విచ్ఛిన్నమవుతుంది మరియు తదుపరి గార్బేజ్ కలెక్షన్ సైకిల్ (garbage collection cycle) సమయంలో V8 ఇంజిన్ కాంపోనెంట్ స్టేట్ మరియు DOM నోడ్లను క్లీన్ చేయడానికి వీలవుతుంది.
విండో వెడల్పును ట్రాక్ చేసే ఒక మోడల్ (modal) గురించి ఆలోచించండి. క్లీనప్ లేకుండా, వినియోగదారు ప్రతిసారీ మోడల్ను తెరిచినప్పుడు, కొత్త లిజనర్ అటాచ్ అవుతుంది. పాతవి ఎప్పటికీ డిటాచ్ అవ్వవు, ఎందుకంటే అవి చెందుతున్న కాంపోనెంట్ ఇన్స్టెన్స్లు పోయాయి, కానీ ఆ ఫంక్షన్లు అనోనిమస్ (anonymous) గా ఉండి పోయాయి. useCallbackలో చుట్టబడిన నేమ్డ్ హ్యాండ్లర్ (named handler) మరియు removeEventListenerని పిలిచే క్లీనప్ ఫంక్షన్ను ఉపయోగించడం వల్ల ఈ లూప్ క్లీన్గా ముగుస్తుంది.
నిజమైన సారాంశం
Reactలో మెమరీ లీక్లు (Memory leaks) అరుదుగా స్పష్టమైన ఎర్రర్ మెసేజ్తో తమను తాము తెలియజేస్తాయి. అవి ఎంత ఎక్కువ సేపు తెరిచి ఉంచితే, ఆ ట్యాబ్ అంత బరువుగా (మెమరీ వినియోగం పెరగడం) మారుతూ తమ ఉనికిని చాటుకుంటాయి. ఏ హుక్ (hook) దీనికి కారణమో ఊహించి సమయాన్ని వృథా చేయకండి. మెమరీ ట్యాబ్ను తెరవండి, గార్బేజ్ కలెక్షన్ను ఫోర్స్ చేయండి మరియు స్నాప్షాట్లను పోల్చండి. హీప్ ప్రొఫైలర్ (heap profiler) మీకు ఖచ్చితమైన రిటైనింగ్ పాత్ను చూపనివ్వండి. ఆ తర్వాత క్లీనప్ ఫంక్షన్ను వ్రాయండి, కాల్బ్యాక్ రిఫరెన్స్ను స్థిరీకరించండి మరియు గ్లోబల్ లింక్ను తెంపివేయండి. మీ వినియోగదారులు ఈ పరిష్కారాన్ని నేరుగా గమనించకపోవచ్చు, కానీ రోజు చివరికి కూడా డ్యాష్బోర్డ్ సాఫీగా నడుస్తుందని వారు గమనిస్తారు.
