तुमच्या वापरकर्त्याचे ब्राउझर टॅब तीस मिनिटांनंतर फ्रीझ होते. UI अडखळते. त्यानंतर ब्राउझर 'out-of-memory' एररसह क्रॅश होतो.

तुम्ही कंपोनंट कोड ओळीनुसार तपासता आणि त्यात काहीही चुकीचे दिसत नाही. React मधील मेमरी लीक्सचा (memory leaks) हाच सर्वात त्रासदायक भाग आहे. हा बग तुमच्या JSX सिंटॅक्समध्ये किंवा तुमच्या हुक लॉजिकमध्ये नसतो. तो तुमच्या कंपोनंट आणि ब्राउझरच्या 'garbage collector' मधील अंतरात असतो. एखादा विसरलेला 'event listener' किंवा दीर्घकाळ टिकणारे 'closure' कलेक्टरला मेमरी रिक्लेम (reclaim) करण्यापासून रोखते. तुम्ही एक कंपोनंट अनमाउंट (unmount) करता, परंतु एकही उरलेला संदर्भ (reference) संपूर्ण ट्रीला 'heap' मध्ये जिवंत ठेवतो. तुम्ही फक्त कोड वाचून हे लीक्स शोधू शकत नाही. ही समस्या पृष्ठभागाखाली लपलेली असते, डोळ्यांना दिसत नाही आणि युनिट टेस्टमध्येही (unit tests) शांत राहते.

लीक्स समोर असूनही का लपलेले असतात

V8 सारखे JavaScript इंजिन मेमरी आपोआप मॅनेज करतात. जेव्हा रूटपासून (root) एखाद्या ऑब्जेक्टपर्यंत कोणताही संदर्भ मार्ग (reference path) उरत नाही, तेव्हा इंजिन त्या ऑब्जेक्टला 'garbage' म्हणून मार्क करते आणि ती जागा रिक्लेम करते. ही प्रक्रिया तोपर्यंत व्यवस्थित काम करते जोपर्यंत एखादा लपलेला संदर्भ तुमच्या इच्छेपेक्षा जास्त काळ टिकून राहत नाही.

React मध्ये, धोका अनेकदा कंपोनंट्स आणि DOM मधील सीमेवर दिसून येतो. तुम्ही एखाद्या मॉडेलमध्ये (modal) window ला resize लिसनर जोडू शकता, किंवा डॅशबोर्ड विजेटमध्ये WebSocket ला सबस्क्राईब करू शकता. जेव्हा वापरकर्ता मॉडेल बंद करतो किंवा दुसऱ्या पेजवर जातो, तेव्हा कंपोनंट अनमाउंट होतो. जर सबस्क्रिप्शन टिकून राहिले, तर इंजिनला ग्लोबल window ऑब्जेक्टपासून तुमच्या हँडलरपर्यंत आणि तुमच्या हँडलरपासून पुन्हा कंपोनंटच्या 'closure' पर्यंत एक वैध संदर्भ दिसतो. कंपोनंट, त्याचे props, त्याचे state आणि DOM नोड्सचे संपूर्ण सबट्री मेमरीमध्ये अडकून (pinned) राहते. शेकडो इंटरॅक्शन्सनंतर, हे अडकलेले ऑब्जेक्ट्स जमा होऊ लागतात. मेमरीचा वापर 'sawtooth pattern' मध्ये वाढतो जो कधीही पूर्णपणे खाली येत नाही.

एंटरप्राइझ डॅशबोर्डची समस्या

हे प्रामुख्याने एंटरप्राइझ डॅशबोर्डमध्ये घडते जिथे वापरकर्ते तासनतास एकाच पेजवर असतात. मॉनिटरिंग पॅनल्स, ॲनालिटिक्स व्ह्यूज किंवा टिकेटिंग सिस्टम्सचा विचार करा. वापरकर्ता एक डिटेल मॉडेल उघडतो, मोठा डेटासेट फिल्टर करतो किंवा सिंगल-पेज ॲपमध्ये टॅब बदलतो. प्रत्येक वैयक्तिक इंटरॅक्शन व्यवस्थित वाटते. मात्र, कालांतराने, 'orphaned nodes' आणि 'detached listeners' जमा होतात. ॲप्लिकेशन केवळ एका महागड्या रेंडरमुळे (expensive render) मंदावत नाही, तर 'heap' इतका मोठा होतो की वारंवार आणि महागडे 'garbage collection pauses' ट्रिगर होतात.

तुमच्या useEffect हुक्सबद्दल अंदाज लावणे थांबवा. लीक अस्तित्वात आहे की नाही हे जाणून घेण्याचा एकमेव मार्ग म्हणजे 'heap' थेट मोजणे. Chrome DevTools तुम्हाला ती दृश्यता (visibility) देते.

Chrome DevTools वापरून लीक्स शोधणे

तुम्हाला एक पुनरुत्पादनीय (reproducible) क्रम आणि काही मिनिटांचे एकाग्र लक्ष आवश्यक आहे. तुमचे ॲप्लिकेशन Chrome मध्ये उघडा, DevTools सुरू करा आणि 'Memory' टॅबवर जा.

बेसलाइन रेकॉर्ड करा. Heap snapshot निवडा आणि Take snapshot वर क्लिक करा. हे सध्या JavaScript heap मध्ये असलेल्या प्रत्येक ऑब्जेक्टला कॅप्चर करते आणि तुम्हाला सुरुवातीची मेमरी दाखवते. हे पेज त्याच्या सुरुवातीच्या 'idle state' मध्ये आल्यानंतर करा, सुरुवातीच्या लोड दरम्यान नाही, जेणेकरून तुम्ही केवळ वापरकर्त्याच्या कृतींमुळे झालेली वाढ मोजू शकाल.

ॲक्शन ट्रिगर करा. ज्या UI इंटरॅक्शनमुळे लीक होत असावा असे तुम्हाला वाटते, तीच कृती करा. एखादे मॉडेल उघडा आणि बंद करा, एखादा कॉम्प्लेक्स चार्ट टॉगल करा, किंवा रूट बदला आणि परत या. एकदा पूर्ण झाले की, ॲप त्याच्या मूळ व्हिज्युअल स्थितीत परत आणा. हे पाऊल अत्यंत महत्त्वाचे आहे. तुम्हाला UI बेसलाइन दरम्यान कसे दिसत होते तसेच दिसावे असे वाटते. जर UI रिकामे दिसत असूनही heap वाढला असेल, तर तुमच्याकडे लीक असल्याचा ठोस पुरावा आहे.

Garbage collection फोर्स करा. Memory टॅबमधील कचरा पेटीच्या (trash can) आयकॉनवर क्लिक करा. हे पूर्ण GC सायकल ट्रिगर करते आणि पुढील कलेक्शनपर्यंत कायदेशीररित्या टिकून राहिलेले तात्पुरते ऑब्जेक्ट्स क्लिअर करते. जे उरते ते म्हणजे खरे लीक्स, असे ऑब्जेक्ट्स ज्यांचे कलेक्शन झाले असते पण अपघाती संदर्भांमुळे (accidental references) ते जिवंत राहिले.

दुसरा स्नॅपशॉट घ्या. पुन्हा Take snapshot वर क्लिक करा. आता तुमच्याकडे एकाच UI परिस्थितीत घेतलेले heap चे दोन फोटो आहेत.

निकाल तुलना करा. व्ह्यू 'Summary' वरून 'Comparison' मध्ये बदला. बेसलाइन म्हणून तुमचा पहिला स्नॅपशॉट आणि तुलना करण्यासाठी दुसरा स्नॅपशॉट सेट करा. Comparison व्ह्यू प्रत्येक ऑब्जेक्ट कॅटेगरीची यादी देतो आणि तुम्हाला 'delta' दाखवतो, म्हणजेच दोन्ही कॅप्चरमधील ऑब्जेक्ट काउंटमधील निव्वळ बदल.

Delta नुसार सॉर्ट करा. ज्या कॅटेगरीमध्ये लक्षणीय वाढ झाली आहे त्या शोधा. विशेषतः Detached HTMLElement आणि React fiber नोड्सवर लक्ष केंद्रित करा. Detached HTML एलिमेंट म्हणजे असा DOM नोड जो आता सक्रिय डॉक्युमेंट ट्रीला जोडलेला नाही, तरीही काही JavaScript संदर्भ त्याला धरून ठेवतात. हे 'smoking guns' (ठोस पुरावे) आहेत. तुम्ही मॉडेल बंद केल्यानंतर किंवा कंपोनंट अनमाउंट केल्यानंतर ते अस्तित्वात नसावेत.

Retaining Path वाचणे

जेव्हा तुम्ही स्नॅपशॉटमध्ये एखादा detached element निवडता, तेव्हा Chrome खालच्या पॅनेलमध्ये retaining path दाखवते. हा मार्ग (path) रूटपासून निवडलेल्या ऑब्जेक्टपर्यंतच्या संदर्भांची (references) एक साखळी असते. त्याचे काळजीपूर्वक अनुसरण करा. तुम्हाला अनेकदा एखादा event listener, IntersectionObserver, setInterval ID, किंवा तुमच्या component मधील विशिष्ट ओळीकडे निर्देश करणारा closure पाहायला मिळेल.

तुम्हाला ओळखीची वाटणारी नावे शोधा. जर तुम्हाला तुमच्या codebase मधील फंक्शन नावासह window ला जोडलेला listener दिसला, तर तुम्हाला 'anchor' सापडला आहे. तो listener धारण करणारा ऑब्जेक्ट तुमचा संपूर्ण component जिवंत ठेवत आहे. कधीकधी ही साखळी एखाद्या third-party library मधून जाते. अशा परिस्थितीत, ती library एखादे explicit teardown call अपेक्षित करते का, जे तुम्ही cleanup function मध्ये कॉल करायला विसरला आहात, हे तपासा.

मूळ कारणांचे निराकरण करणे

एकदा का तुम्ही retaining path ओळखला की, त्याचे निराकरण सहसा तांत्रिक (mechanical) असते, परंतु त्यासाठी टीममध्ये शिस्त असणे आवश्यक आहे.

Cleanup functions वापरा. जेव्हा तुम्ही window किंवा document ला listeners जोडता, तेव्हा useEffect मध्ये नेहमी एक cleanup function return करा. जर तुमचा effect एखाद्या resize event ला subscribe करत असेल, तर component unmount होण्यापूर्वी ते subscription काढून टाका. React जेव्हा component tear down करते, तेव्हा cleanup रन होते, ज्यामुळे तुम्हाला बाह्य कनेक्शन तोडण्यासाठी एक खात्रीशीर hook मिळतो.

References स्थिर करा (Stabilize references). तुमचे handlers useCallback मध्ये wrap करा. यामुळे तुम्ही removeEventListener ला अगदी तोच function reference पास करता जो तुम्ही मूळतः addEventListener ला पास केला होता. जर तुम्ही window.addEventListener('resize', () => { ... }) सारखे inline function रजिस्टर केले आणि नंतर ते दुसऱ्या inline function ने काढण्याचा प्रयत्न केला, तर references जुळणार नाहीत. परिणामी listener जोडलेलाच राहतो. त्यातील closure तुमच्या component state ला जिवंत ठेवते. स्थिर dependency array सह useCallback वापरल्यामुळे ही identity mismatch टाळता येते.

Global links तोडा. लक्षात ठेवा की ब्राउझरचे event target objects, जसे की window आणि document, पेजच्या संपूर्ण आयुष्यासाठी (lifetime) अस्तित्वात असतात. त्यातून तुमच्या component कडे जाणारा कोणताही reference हा 'global anchor' म्हणून काम करतो. Listener काढून टाकल्यामुळे तो anchor तुटतो आणि V8 engine ला पुढील garbage collection cycle दरम्यान component state आणि DOM nodes काढून टाकण्यास मदत होते.

विंडोची रुंदी (width) ट्रॅक करणाऱ्या एका modal चा विचार करा. Cleanup शिवाय, प्रत्येक वेळी वापरकर्ता modal उघडतो, तेव्हा एक नवीन listener जोडला जातो. जुने listeners कधीच detach होत नाहीत कारण ज्या component instances शी ते संबंधित होते ते आता अस्तित्वात नाहीत, तरीही ती functions anonymous होती आणि हरवली आहेत. useCallback मध्ये wrap केलेले एक named handler आणि removeEventListener कॉल करणारे cleanup function वापरल्यामुळे ही प्रक्रिया व्यवस्थित पूर्ण होते.

महत्त्वाचा निष्कर्ष

React मधील memory leaks सहसा स्पष्ट error message देऊन स्वतःची ओळख करून देत नाहीत. ते अशा टॅबद्वारे स्वतःची उपस्थिती दर्शवतात जो जितका जास्त वेळ उघडा राहील तितका जड होत जातो. कोणता hook दोषी आहे याचा अंदाज लावण्यात वेळ वाया घालवू नका. Memory tab उघडा, garbage collection फोर्स करा आणि snapshots ची तुलना करा. Heap profiler ला तुम्हाला नेमका retaining path दाखवू द्या. त्यानंतर cleanup function लिहा, callback reference स्थिर करा आणि global link तोडा. तुमच्या वापरकर्त्यांना हे निराकरण थेट जाणवणार नाही, परंतु दिवसाच्या शेवटी डॅशबोर्ड अजूनही सुरळीतपणे चालत आहे, हे त्यांना नक्कीच जाणवेल.