आपके यूजर का ब्राउज़र टैब तीस मिनट के बाद फ्रीज हो जाता है। UI अटकने लगता है। फिर ब्राउज़र 'out-of-memory' एरर के साथ क्रैश हो जाता है।

आप कंपोनेंट कोड की लाइन-दर-लाइन समीक्षा करते हैं और आपको कुछ भी गलत नहीं दिखता। React में मेमोरी लीक का यही सबसे परेशान करने वाला हिस्सा है। बग आपके JSX सिंटैक्स या आपके hook लॉजिक में नहीं होता है। यह आपके कंपोनेंट और ब्राउज़र के garbage collector के बीच के अंतर में छिपा होता है। एक भटकता हुआ (stray) event listener या एक लंबे समय तक चलने वाला closure कलेक्टर को मेमोरी वापस लेने (reclaim) से रोकता है। आप एक कंपोनेंट को unmount करते हैं, लेकिन एक अकेला बचा हुआ reference पूरे tree को heap में जीवित रखता है। आप केवल कोड पढ़कर इन लीक्स को नहीं ढूंढ सकते। समस्या सतह के नीचे छिपी रहती है, जो आंखों से ओझल और unit tests में शांत रहती है।

लीक्स (Leaks) सामने होते हुए भी क्यों छिप जाते हैं

V8 जैसे JavaScript engines मेमोरी को स्वचालित रूप से मैनेज करते हैं। जब root से किसी object तक कोई reference path नहीं बचता, तो engine उस object को garbage के रूप में मार्क कर देता है और उस जगह को खाली (reclaim) कर देता है। यह प्रक्रिया तब तक अच्छी तरह काम करती है जब तक कि कोई छिपा हुआ reference आपकी योजना से अधिक समय तक जीवित न रहे।

React में, खतरा अक्सर components और DOM के बीच की सीमा पर दिखाई देता है। आप किसी modal के अंदर window पर resize listener लगा सकते हैं, या किसी dashboard widget में WebSocket को subscribe कर सकते हैं। जब यूजर modal बंद करता है या कहीं और नेविगेट करता है, तो component unmount हो जाता है। यदि subscription जीवित रहती है, तो engine को global window object से लेकर आपके handler तक, और आपके handler से वापस component के closure तक एक वैध reference दिखाई देता है। Component, उसके props, उसका state, और DOM nodes का उसका पूरा subtree मेमोरी में 'pinned' (अटक) जाता है। सैकड़ों इंटरैक्शन के बाद, ये pinned objects जमा होने लगते हैं। मेमोरी का उपयोग एक sawtooth pattern में बढ़ता है जो कभी भी पूरी तरह से नीचे नहीं गिरता।

एंटरप्राइज डैशबोर्ड की समस्या

ऐसा सबसे ज्यादा एंटरप्राइज डैशबोर्ड में होता है जहाँ यूजर्स घंटों तक एक ही पेज पर रहते हैं। मॉनिटरिंग पैनल, एनालिटिक्स व्यू, या टिकटिंग सिस्टम के बारे में सोचें। एक यूजर एक detail modal खोलता है, एक बड़े dataset को filter करता है, या single-page app के भीतर tabs बदलता है। हर व्यक्तिगत इंटरैक्शन ठीक लगता है। हालाँकि, समय के साथ, orphaned nodes और detached listeners जमा होने लगते हैं। एप्लिकेशन किसी एक महंगे render के कारण नहीं, बल्कि इसलिए धीमा हो जाता है क्योंकि heap इतना बड़ा हो जाता है कि बार-बार और महंगे garbage collection pauses होने लगते हैं।

अपने useEffect hooks के बारे में अंदाज़ा लगाना बंद करें। यह जानने का एकमात्र तरीका कि कोई लीक मौजूद है या नहीं, सीधे heap को मापना है। Chrome DevTools आपको वह विजिबिलिटी देता है।

Chrome DevTools के साथ लीक्स को ढूंढना

आपको एक reproducible sequence और कुछ मिनटों के केंद्रित ध्यान की आवश्यकता है। Chrome में अपना एप्लिकेशन खोलें, DevTools लॉन्च करें, और Memory tab पर जाएँ।

एक baseline रिकॉर्ड करें। Heap snapshot चुनें और Take snapshot पर क्लिक करें। यह JavaScript heap में वर्तमान में जीवित हर object को कैप्चर करता है और आपको शुरुआती मेमोरी दिखाता है। ऐसा पेज के अपने शुरुआती idle state में स्थिर होने के बाद करें, न कि initial load के दौरान, ताकि आप केवल यूजर के कार्यों के कारण होने वाली वृद्धि को माप सकें।

एक्शन को ट्रिगर करें। ठीक वही UI इंटरैक्शन करें जिसका आपको संदेह है कि वह लीक का कारण बनता है। एक modal खोलें और बंद करें, एक जटिल chart को toggle करें, या route बदलें और वापस नेविगेट करें। समाप्त होने के बाद, ऐप को उसकी मूल विजुअल स्थिति में वापस लाएँ। यह चरण महत्वपूर्ण है। आप चाहते हैं कि UI बिल्कुल वैसा ही दिखे जैसा baseline के दौरान दिख रहा था। यदि UI खाली दिखने के बावजूद heap बढ़ गया है, तो आपके पास लीक का पुख्ता सबूत है।

Garbage collection को फोर्स करें। Memory tab में कचरे के डिब्बे (trash can) वाले आइकन पर क्लिक करें। यह एक full GC cycle को ट्रिगर करता है और उन अस्थायी objects को साफ़ कर देता है जो वैध रूप से अगली collection तक जीवित रहे। जो बच जाता है वह असली लीक है—वे objects जिन्हें collect किया जाना चाहिए था लेकिन accidental references के कारण जीवित रखे गए थे।

दूसरा snapshot लें। फिर से Take snapshot पर क्लिक करें। अब आपके पास एक ही UI स्थितियों के तहत लिए गए heap के दो फोटोग्राफ हैं।

परिणामों की तुलना करें। View को Summary से बदलकर Comparison कर दें। Baseline के रूप में अपने पहले snapshot को और compared snapshot के रूप में दूसरे को सेट करें। Comparison view हर object category को सूचीबद्ध करता है और आपको delta दिखाता है, जो दोनों कैप्चर के बीच object counts में शुद्ध परिवर्तन (net change) है।

Delta के आधार पर सॉर्ट करें। उन categories को देखें जो काफी बढ़ गई हैं। विशेष रूप से Detached HTMLElement और React fiber nodes पर ध्यान दें। एक detached HTML element एक ऐसा DOM node है जो अब active document tree से जुड़ा नहीं है, फिर भी कोई JavaScript reference उसे पकड़े हुए है। ये 'smoking guns' (स्पष्ट प्रमाण) हैं। modal बंद करने या component को unmount करने के बाद इनका अस्तित्व नहीं होना चाहिए।

Retaining Path को पढ़ना

जब आप स्नैपशॉट में किसी detached element को चुनते हैं, तो Chrome नीचे के पैनल में retaining path दिखाता है। यह पाथ रूट (root) से लेकर चुने गए ऑब्जेक्ट तक के रेफरेंस की एक चेन है। इसे ध्यान से फॉलो करें। आपको अक्सर एक event listener, एक IntersectionObserver, एक setInterval ID, या आपके कंपोनेंट की किसी विशिष्ट लाइन की ओर इशारा करने वाला एक closure मिलेगा।

उन नामों को खोजें जिन्हें आप पहचानते हैं। यदि आप अपने codebase से किसी फंक्शन नाम के साथ window से जुड़ा हुआ एक listener देखते हैं, तो आपको anchor मिल गया है। वह ऑब्जेक्ट जो उस listener को थामे हुए है, आपके पूरे कंपोनेंट को जीवित रख रहा है। कभी-कभी यह चेन किसी third-party library के माध्यम से चलती है। ऐसे मामलों में, जाँचें कि क्या लाइब्रेरी किसी explicit teardown call की अपेक्षा करती है जिसे आप cleanup function में कॉल करना भूल गए हैं।

मूल कारणों को ठीक करना

एक बार जब आप retaining path की पहचान कर लेते हैं, तो इसका समाधान आमतौर पर मैकेनिकल होता है लेकिन इसके लिए पूरी टीम में अनुशासन की आवश्यकता होती है।

cleanup functions का उपयोग करें। जब आप window या document में listeners जोड़ते हैं, तो हमेशा useEffect में एक cleanup function return करें। यदि आपका effect किसी resize event को subscribe करता है, तो कंपोनेंट unmount होने से पहले उस subscription को हटा दें। cleanup तब चलता है जब React कंपोनेंट को tear down करता है, जिससे आपको बाहरी कनेक्शनों को तोड़ने के लिए एक गारंटीड hook मिलता है।

references को स्थिर (stabilize) करें। अपने handlers को useCallback में wrap करें। यह सुनिश्चित करता है कि आप removeEventListener को ठीक वही function reference पास करें जो आपने मूल रूप से addEventListener को पास किया था। यदि आप window.addEventListener('resize', () => { ... }) जैसा कोई inline function रजिस्टर करते हैं और बाद में उसे किसी अन्य inline function के साथ हटाने की कोशिश करते हैं, तो references मेल नहीं खाएंगे। Listener जुड़ा रहेगा। इसके अंदर का closure आपके कंपोनेंट state को जीवित रखता है। एक stable dependency array के साथ useCallback इस identity mismatch को रोकता है।

global links को तोड़ें। याद रखें कि ब्राउज़र के event target objects, जैसे window और document, पेज के जीवनकाल तक रहते हैं। उनमें से आपके कंपोनेंट में कोई भी reference एक global anchor के रूप में कार्य करता है। Listener को हटाने से वह anchor टूट जाता है और V8 engine को अगले garbage collection cycle के दौरान component state और DOM nodes को साफ करने की अनुमति मिल जाती है।

एक modal के बारे में सोचें जो window width को ट्रैक करता है। Cleanup के बिना, हर बार जब उपयोगकर्ता modal खोलता है, तो एक नया listener जुड़ जाता है। पुराने वाले कभी अलग नहीं होते क्योंकि वे जिस component instances से संबंधित हैं वे खत्म हो चुके होते हैं, फिर भी वे functions स्वयं anonymous होते हैं और खो जाते हैं। useCallback में wrap किया गया एक named handler, और साथ ही एक cleanup function जो removeEventListener को कॉल करता है, इस लूप को सफाई से बंद कर देता है।

एक वास्तविक निष्कर्ष

React में memory leaks शायद ही कभी किसी स्पष्ट error message के साथ खुद को प्रकट करते हैं। वे खुद को एक ऐसे tab के माध्यम से प्रकट करते हैं जो जितना अधिक खुला रहता है, उतना ही भारी होता जाता है। इस बारे में अनुमान लगाने में समय बर्बाद न करें कि कौन सा hook दोषी है। Memory tab खोलें, garbage collection को force करें, और snapshots की तुलना करें। Heap profiler को आपको सटीक retaining path दिखाने दें। फिर cleanup function लिखें, callback reference को स्थिर करें, और global link को काट दें। आपके उपयोगकर्ता सीधे तौर पर इस सुधार को नोटिस नहीं करेंगे, लेकिन वे यह नोटिस करेंगे कि दिन के अंत में भी डैशबोर्ड सुचारू रूप से चलता रहता है।