మీ యాప్ పది నిమిషాల పాటు బాగానే నడుస్తుంది. ఆ తర్వాత స్క్రోలింగ్ నెమ్మదించడం (sticky) మొదలవుతుంది. అరగంట తర్వాత ట్యాబ్ ఒక గిగాబైట్ మెమరీని చేరుకుంటుంది. చివరికి, 'out-of-memory' ఎర్రర్‌తో పేజీ ఆగిపోతుంది మరియు మీకు దానికి సంబంధించిన స్టాక్ ట్రేస్ (stack trace) కూడా ఉండదు.

ఇది రెండర్ పెర్ఫార్మెన్స్ (render performance) సమస్య కాదు. కాంపోనెంట్స్ ఎంత తరచుగా రీడ్రా (redraw) అవుతున్నాయి అనేది ఇక్కడ సమస్య కాదు కాబట్టి, React DevTools Profiler అంతా మామూలుగానే కనిపిస్తుంది. అసలు సమస్య ఏమిటంటే, కాంపోనెంట్స్ అన్‌మౌంట్ (unmount) అయిన తర్వాత కూడా ఏవి మెమరీలో ఉండిపోతున్నాయి అనేది. JavaScript heap లో ఎక్కడో ఉన్న ఒక అదనపు రిఫరెన్స్ (stray reference), మొత్తం DOM నోడ్స్, క్లోజర్స్ (closures) మరియు స్టేట్ (state) యొక్క ట్రీని పట్టి ఉంచుతుంది. బ్రౌజర్ వీటిని తిరిగి వాడుకోలేదు (reclaim), దీనివల్ల మెమరీ పెరిగిపోతూ చివరికి ప్రాసెస్ క్రాష్ అవుతుంది.

మీ సోర్స్ కోడ్ చదవడం వల్ల ఈ లీక్ (leak) ఎక్కడ ఉందో తెలియదు. మీరు ఏది అన్‌మౌంట్ అయిందని అనుకుంటున్నారో, దానికి మరియు గార్బేజ్ కలెక్టర్ (garbage collector) నిజంగా దేనిని చూస్తుందో, ఆ రెండింటి మధ్య ఉన్న వ్యత్యాసంలోనే ఈ బగ్ ఉంటుంది. V8 కేవలం 'zero retaining paths' ఉన్న ఆబ్జెక్ట్‌లను మాత్రమే ఫ్రీ చేస్తుంది. ఒక అన్-క్లియర్డ్ అబ్జర్వర్ (uncleared observer), లేదా ఒక లాంగ్-లివ్డ్ క్లోజర్ (long-lived closure), లేదా ఏదైనా ఈవెంట్ లిజనర్ (event listener) ఒక ఫైబర్ (fiber) లేదా DOM నోడ్‌కు ఒక్క పాయింటర్‌ను పట్టి ఉంచినా, మొత్తం కాంపోనెంట్ సబ్‌ట్రీ (subtree) మెమరీలో ఉండిపోతుంది. మీరు ఒక మోడల్‌ను (modal) అన్‌మౌంట్ చేసినా, window లో ఉన్న ఒక లిజనర్ ఆ మోడల్‌లో నిర్వచించిన హ్యాండ్లర్‌ను (handler) ఇంకా పాయింట్ చేస్తూ ఉండటం వల్ల, దాని డిటాచ్డ్ నోడ్స్ (detached nodes) మెమరీలో ఉండిపోతాయి.

లీక్ ఎక్కడ ఉందో నిరూపించడానికి మీరు ఎడిటర్‌ను కాకుండా, హీప్ (heap) ను చూడాలి.

హీప్ (Heap) ఎందుకు నిజం చెబుతుంది

గార్బేజ్ కలెక్టర్ దేనిని చూస్తుందో తెలుసుకోవడానికి Chrome DevTools మీకు నేరుగా అవకాశం ఇస్తుంది. Memory ట్యాబ్ ద్వారా మీరు హీప్ స్నాప్‌షాట్‌లను (heap snapshots) రికార్డ్ చేయవచ్చు: అంటే ప్రస్తుతం JavaScript మెమరీలో ఉన్న ప్రతి ఆబ్జెక్ట్, DOM నోడ్ మరియు క్లోజర్ యొక్క పూర్తి జాబితా. లీక్ జరుగుతుందని అనుమానించే ముందు మరియు తర్వాత తీసిన రెండు స్నాప్‌షాట్‌లను పోల్చడం ద్వారా, ఏ ఆబ్జెక్ట్‌లు మెమరీ నుండి తొలగించబడలేదో మీరు ఖచ్చితంగా గుర్తించవచ్చు.

ఇది కేవలం సిద్ధాంతం మాత్రమే కాదు. లీక్ అయిన ఒకే ఒక React కాంపోనెంట్ వేల సంఖ్యలో డిటాచ్డ్ HTMLElement ఆబ్జెక్ట్‌లను మెమరీలో ఉంచగలదు. ఆ ఆబ్జెక్ట్‌లు ఇకపై కనిపించే డాక్యుమెంట్‌కు అనుసంధానించబడి ఉండవు, కానీ JavaScript రిఫరెన్స్‌లు వాటిని కలెక్ట్ చేయకుండా అడ్డుకుంటాయి. ఇవి కంపారిజన్ వ్యూలో Detached HTMLElement అనే కన్‌స్ట్రక్టర్ పేరుతో కనిపిస్తాయి. ఇవి పెరుగుతూ ఉండటం మీరు గమనిస్తే, లీక్ ఎక్కడ ఉందో మీరు కనుగొన్నట్లే.

Chrome DevTools వర్క్‌ఫ్లో (Workflow)

శుభ్రంగా ప్రారంభించండి. సంబంధం లేని బ్రౌజర్ ట్యాబ్‌లను మూసివేయండి, అనవసరమైన ఎక్స్‌టెన్షన్‌లను డిసేబుల్ చేయండి మరియు మీ అప్లికేషన్ స్థిరమైన స్థితికి (steady state) వచ్చేలా చూడండి. Chrome DevTools ఓపెన్ చేసి, Memory ట్యాబ్‌కు వెళ్లి, Heap snapshot ని ఎంచుకోండి. 'Take snapshot' పై క్లిక్ చేయండి. ఇది మీ ప్రారంభ మెమరీ ఫుట్‌ప్రింట్‌ను (memory footprint) క్యాప్చర్ చేస్తుంది.

ఇప్పుడు మీరు అనుమానిస్తున్న అదే యూజర్ యాక్షన్‌ను చేయండి. ఆ భారీ మోడల్‌ను ఓపెన్ చేసి క్లోజ్ చేయండి. విడ్జెట్‌ను మౌంట్ (mount) మరియు అన్‌మౌంట్ (unmount) చేయండి. ఒక రూట్‌కు వెళ్లి మళ్ళీ వెనక్కి రండి. UI తిరిగి దాని అసలు స్థితికి వచ్చిన తర్వాత, Memory ట్యాబ్‌లోని ట్రాష్ కాన్ (trash can) ఐకాన్‌పై క్లిక్ చేయండి. ఇది గ్లోబల్ గార్బేజ్ కలెక్షన్ (global garbage collection) ప్రక్రియను బలవంతంగా చేస్తుంది. రెండర్ సైకిల్ నుండి వచ్చిన తాత్కాలిక ఆబ్జెక్ట్‌లు తొలగిపోవాలి. ఏవి మిగిలిపోతాయో, అవి లీక్ అయ్యే అవకాశం ఉన్న ఆబ్జెక్ట్‌లు.

మళ్ళీ 'Take snapshot' పై క్లిక్ చేయండి. ఇప్పుడు మీ వద్ద మెమరీ యొక్క రెండు ఫోటోలు ఉన్నాయి. వ్యూను Summary నుండి Comparison కి మార్చండి. కంపారిజన్ స్కోప్‌ను మొదటి స్నాప్‌షాట్‌గా సెట్ చేయండి. రన్‌టైమ్ (runtime) యొక్క అనవసరమైన డేటాను తొలగించి, ఆ రెండు క్యాప్చర్‌ల మధ్య ఏవి మారాయో మాత్రమే ఈ టూల్ మీకు చూపిస్తుంది.

Delta ద్వారా సార్ట్ చేయండి. పెరిగిన ఆబ్జెక్ట్ కౌంట్‌లను చూడండి. Detached HTMLElement, Array, Function వంటి కన్‌స్ట్రక్టర్ల మీద లేదా మీ స్వంత కోడ్‌బేస్‌లోని క్లాస్ ఇన్‌స్టెన్స్‌ల (class instances) మీద ప్రత్యేక శ్రద్ధ వహించండి. పెరుగుతున్న డెల్టా (delta) అంటే, మీ యాక్షన్ సమయంలో ఆబ్జెక్ట్‌లు సృష్టించబడ్డాయి మరియు అవి తర్వాత కలెక్ట్ చేయబడలేదని అర్థం.

రిటైనింగ్ పాత్ (Retaining Path) ను ట్రాస్ చేయండి

లీక్ అయిన ఎలిమెంట్‌ను గుర్తించినప్పుడు, దానిని సెలెక్ట్ చేయండి. దిగువ ప్యానెల్ రిటైనింగ్ పాత్‌ను (retaining path) చూపిస్తుంది: అంటే ఈ ఆబ్జెక్ట్ ఇంకా ఎందుకు బ్రతికి ఉందో వివరించే రిఫరెన్స్‌ల గొలుసు (chain of references). ఈ గొలుసు ఒక డిటాచ్డ్ div నుండి మొదలై, React అంతర్గత ప్రాపర్టీల ద్వారా, ఒక క్లోజర్‌లోకి వెళ్లి, చివరకు మీ కాంపోనెంట్స్‌లో రిజిస్టర్ చేయబడిన ఒక ఈవెంట్ లిజర్‌పై ముగుస్తుంది. ఆ గొలుసులోని చివరి లింక్ మీ లైన్ నంబర్ (line number).

ఇక్కడే మీరు సమస్యను గుర్తించడం నుండి దాని మూల కారణాన్ని (root cause) కనుగొనే దశకు చేరుకుంటారు. రిటైనింగ్ పాత్ window.addEventListener వద్ద ముగిస్తే, ఒక గ్లోబల్ లిజనర్ మీ కాంపోనెంట్‌ను పట్టి ఉంచుతోందని మీకు తెలుస్తుంది. అది IntersectionObserver ఇన్‌స్టెన్స్ వద్ద ముగిస్తే, గార్బేజ్ కలెక్ట్ అయి ఉండాల్సిన ఒక నోడ్‌ను అబ్జర్వర్ ఇంకా గమనిస్తూ ఉందని మీకు అర్థమవుతుంది.

React లో సాధారణంగా కనిపించే కారణాలు

React లో మెమరీ లీక్‌లు సాధారణంగా మూడు రకాలుగా ఉంటాయి.

Orphaned global listeners. స్క్రోల్ పొజిషన్, కీ ప్రెస్‌లు లేదా రీసైజ్ ఈవెంట్‌లను ట్రాక్ చేయడానికి ఒక useEffect అనేది window లేదా document కు హుక్ అవుతుంది. ఒకవేళ ఆ ఎఫెక్ట్ removeEventListenerని పిలిచే క్లీనప్ ఫంక్షన్‌ను (cleanup function) రిటర్న్ చేయకపోతే, ఆ లిజనర్ పేజీ అంతా ఉండే వరకు అలాగే ఉంటుంది. ఆ లిజనర్ ఒక క్లోజర్ కాబట్టి, React కాంపోనెంట్‌ను అన్‌మౌంట్ చేసిన తర్వాత కూడా అది మొత్తం కాంపోనెంట్ స్కోప్‌ను బ్రతికి ఉంచుతుంది.

Uncleared observers. IntersectionObserver మరియు ResizeObserver శక్తివంతమైనవి, కానీ అవి React నియంత్రణకు వెలుపల నేటివ్ రిఫరెన్స్‌లను (native references) సృష్టిస్తాయి. మీరు ఒక కాంపోనెంట్ లోపల అబ్జర్వర్‌ను ఇన్‌స్టాంటియేట్ చేసి, క్లీనప్ దశలో disconnect() కాల్ చేయడం మర్చిపోతే, ఆ అబ్జర్వర్ టార్గెట్ DOM నోడ్‌ను పట్టుకుని ఉంటుంది, మరియు ఆ DOM నోడ్ React fibers, props, మరియు stateలను పట్టుకుని ఉంటుంది.

Closure traps. మీరు ఒక కాంపోనెంట్ లోపల ఒక ఫంక్షన్‌ను నిర్వచించి, దానిని థర్డ్-పార్టీ లైబ్రరీకి, గ్లోబల్ క్యాష్‌కు లేదా setTimeoutకి పంపినప్పుడు, ఆ ఫంక్షన్ దాని లెక్సికల్ స్కోప్‌లోని ప్రతి వేరియబుల్‌ను క్లోజ్ చేస్తుంది. ఒకవేళ ఆ ఎక్స్‌టర్నల్ ఓనర్ ఆ ఫంక్షన్‌ను ఉంచుకుంటే, అది మీ పూర్తి కాంపోనెంట్ స్కోప్‌ను కూడా తనతో పాటు ఉంచుకుంటుంది.

నిజంగా పనిచేసే క్లీనప్ ప్యాటర్న్స్ (Cleanup Patterns That Actually Work)

లీక్‌ను సరిదిద్దడం అంటే స్నాప్‌షాట్‌లో మీరు కనుగొన్న ప్రతి రిటెయినింగ్ పాత్‌ను (retaining path) తెంపివేయడం.

useEffect నుండి ఎల్లప్పుడూ క్లీనప్ ఫంక్షన్‌ను రిటర్న్ చేయండి. మీరు ఎఫెక్ట్‌లో ఒక లిజనర్‌ను యాడ్ చేస్తే, అక్కడే దానిని తొలగించండి.

మీరు DOM లేదా windowకి అటాచ్ చేసే ఏ హ్యాండ్లర్‌కైనా useCallback ఉపయోగించండి. అది లేకపోతే, ప్రతి రెండర్ ఒక కొత్త ఫంక్షన్ రిఫరెన్స్‌ను సృష్టిస్తుంది. మీరు ఒక రిఫరెన్స్‌తో addEventListenerని కాల్ చేసి, తర్వాత వేరొక రిఫరెన్స్‌తో removeEventListenerని కాల్ చేస్తే, ఆ తొలగింపు నిశ్శబ్దంగా విఫలమవుతుంది. అసలు లిజనర్ windowలో శాశ్వతంగా ఉండిపోతుంది. useCallback రిఫరెన్స్‌ను స్థిరంగా ఉంచుతుంది, తద్వారా add మరియు remove ఖచ్చితంగా సరిపోలుతాయి.

అబ్జర్వర్లను కూడా అదే క్రమశిక్షణతో హ్యాండిల్ చేయండి. అబ్జర్వర్ ఇన్‌స్టన్స్‌ను ఎఫెక్ట్ లోపల ఒక ref లేదా లోకల్ వేరియబుల్‌లో స్టోర్ చేయండి. క్లీనప్ ఫంక్షన్‌లో, observer.disconnect()ని కాల్ చేయండి. కాంపోనెంట్ అన్‌మౌంట్ (unmounting) చేయడం వల్ల అబ్జర్వర్ ఆగిపోతుందని అనుకోవద్దు. అది జరగదు.

మీ కాంపోనెంట్ ఏదైనా గ్లోబల్ నేమ్‌స్పేస్ లేదా సింగిల్టన్ సర్వీస్‌కు ఏదైనా పబ్లిష్ చేస్తే, అన్‌మౌంట్ చేసేటప్పుడు ఆ రిఫరెన్స్‌లను డిలీట్ చేయండి. ఒక ఆబ్జెక్ట్ నిజంగా అన్‌రీచబుల్ (unreachable) అయినప్పుడు మాత్రమే V8 ఇంజిన్ మెమరీని తిరిగి పొందగలదు. windowలో ఒక హుక్‌ను లేదా మాడ్యూల్-లెవల్ Mapలో ఒక ఎంట్రీని వదిలేయడం వల్ల ఒక అదృశ్య వంతెన ఏర్పడుతుంది, ఇది హీప్ (heap) నిరంతరం పెరిగేలా చేస్తుంది.

అసలైన సారాంశం (The Real Takeaway)

మెమరీ లీక్‌లు మీ యాప్‌ను వెంటనే క్రాష్ చేయవు. సుదీర్ఘమైన యూజర్ సెషన్ల సమయంలో అవి ఒక్కో డీటాచ్డ్ నోడ్‌ను పేరుకుపోతాయి. దీని పరిష్కారం లైబ్రరీ అప్‌గ్రేడ్ లేదా కంపైలర్ ఫ్లాగ్ కాదు. హీప్ స్నాప్‌షాట్‌లతో మీ క్లీనప్ లాజిక్‌ను నిరూపించుకోవడం అలవాటు చేసుకోవడం.

ఒక బేస్‌లైన్‌ను తీసుకోండి, అనుమానిత ఫ్లోను ట్రిగ్గర్ చేయండి, గార్బేజ్ కలెక్షన్‌ను ఫోర్స్ చేయండి మరియు పోల్చండి. ఒకవేళ డెల్టా (delta) పెరుగుదలని చూపిస్తే, రిటెయినింగ్ పాత్‌ను తనిఖీ చేయండి, ఉండకూడని లిజనర్ లేదా అబ్జర్వర్‌ను కనుగొని, ఆ రిఫరెన్స్‌ను కట్ చేయండి. పరీక్షను మళ్ళీ రన్ చేయండి. డెల్టా స్థిరంగా ఉన్నప్పుడు, మీరు దానిని నిజంగా పరిష్కరించారు అని అర్థం. మీ అప్లికేషన్ రెస్పాన్సివ్‌గా ఉంటుంది మరియు మీ యూజర్లు ఫ్రీజ్ అయిన బ్రౌజర్ ట్యాబ్ వల్ల తమ పనిని కోల్పోరు.