WeakMap से मेमोरी लीक रोकें
कमज़ोर रिफ़रेंस से कमज़ोर कोड ठीक करना!
क्या आपने कभी वो एहसास महसूस किया जब एक लाइन बदलाव से मेमोरी उपयोग आधा हो जाता है? मैंने ये Chrome के DevTools Performance Monitor में देखा जब एक डैशबोर्ड ऐप घंटों में 100MB खोने से लेकर पूरे दोपहर तक स्थिर चलने लगा।
एक लाइन का बदलाव: new Map() बन गया new WeakMap()।
बस इतना। वही API, वही इस्तेमाल का तरीका, पर अंदर से पूरी तरह अलग व्यवहार। लेकिन ये समझने के लिए कि ये क्यों काम करता है, आपको समझना होगा कुछ ऐसा जिसके बारे में ज़्यादातर JavaScript डेवलपर कभी नहीं सोचते: जब कोई आपके डेटा को नहीं देख रहा, तो उसका क्या होता है।
जब रिफ़रेंस एंकर बन जाते हैं
JavaScript में एक सामान्य Map अपनी कीज़ को कीमती सामान की तरह समझता है। जब आप कुछ उसमें डालते हैं, Map उसे मज़बूती से पकड़े रखता है। Garbage Collector ये रिश्ता देखकर सोचता है, “साफ़ है, इन्हें अभी भी इस ऑब्जेक्ट की ज़रूरत है, बेहतर है हाथ न लगाऊँ।”
ये सुरक्षात्मक स्वभाव तब समस्या बनता है जब आप अस्थायी चीज़ों के बारे में मेटाडेटा स्टोर कर रहे हों। DOM नोड्स जो हटा दिए जाते हैं। यूज़र सेशन जो एक्सपायर होते हैं। कंपोनेंट इंस्टेंसेज़ जो अनमाउंट होते हैं। Map को नहीं पता कि ये ऑब्जेक्ट्स काम पूरा कर चुके हैं। उसे बस इतना पता है कि उसके पास एक रिफ़रेंस है, तो वो उन्हें ज़िंदा रखता है।
const cache = new Map();
function trackClick(element) { cache.set(element, { clicks: 0 });}
document.body.removeChild(element);// element DOM से गया, पर cache उसे मेमोरी में रखे हुए हैGarbage Collector element को क्लीनअप नहीं कर सकता क्योंकि cache अभी भी उसे पॉइंट कर रहा है। इसे “strong reference” कहते हैं, और लंबे समय तक चलने वाले Single Page Apps में ये एक लीक बन जाता है जो आखिरकार ब्राउज़र को क्रैश कर देता है।
WeakMap नियम बदल देता है
WeakMap अलग तरह से काम करता है। ये अपनी कीज़ को स्थायी निवासी की बजाय अस्थायी नागरिक मानता है। जब आप WeakMap में कुछ स्टोर करते हैं, तो आप कह रहे होते हैं: “मैं इस डेटा को इस ऑब्जेक्ट से जोड़ना चाहता हूँ, पर मैं नहीं चाहता कि मेरी वजह से ये ज़िंदा रहे।”
अगर किसी ऑब्जेक्ट को मेमोरी में रखने वाली एकमात्र चीज़ WeakMap है, तो Garbage Collector उसे हटा सकता है। जब ऑब्जेक्ट गायब होता है, WeakMap की एंट्री भी अपने आप गायब हो जाती है। किसी मैनुअल क्लीनअप की ज़रूरत नहीं।
const cache = new WeakMap();
function trackClick(element) { cache.set(element, { clicks: 0 });}
document.body.removeChild(element);// element Garbage Collect हो जाता है// cache की एंट्री अपने आप गायब हो जाती हैमैंने एक बेंचमार्क चलाया जिसमें 100,000 DOM नोड्स बनाए, हर एक के बारे में मेटाडेटा स्टोर किया, फिर सब हटा दिए। Map के साथ ब्राउज़र 150-200MB पकड़े रहा। WeakMap के साथ ये 70-80MB तक गिर गया। वही कोड, वही कार्यक्षमता, आधी मेमोरी खपत।
आप क्या छोड़ते हैं
WeakMap की कुछ बंदिशें सीमाएँ लगती हैं, जब तक आप समझते हैं कि यही उसे खास बनाती हैं।
आप WeakMap पर इटरेट नहीं कर सकते। कोई forEach नहीं, कोई keys() नहीं, कोई values() नहीं। सोचने पर ये समझ में आता है: Garbage Collector आपके लूप के बीच में एक एंट्री डिलीट कर सकता है। क्या आप सच में इससे निपटना चाहेंगे?
आप साइज़ नहीं देख सकते। कोई .size प्रॉपर्टी नहीं, कोई .length नहीं। फिर से, ये एक मूविंग टारगेट है। नंबर आपके पूछने और जवाब मिलने के बीच बदल सकता है।
कीज़ ऑब्जेक्ट होनी चाहिए। कोई स्ट्रिंग्स नहीं, कोई नंबर्स नहीं, कोई प्रिमिटिव्स नहीं। ये weak references के काम करने के तरीके के लिए ज़रूरी है: प्रिमिटिव वैल्यूज़ की कोई ऐसी पहचान नहीं होती जो उनके वैल्यू से अलग ट्रैक की जा सके।
ये बग्स नहीं हैं। ये डिज़ाइन है। WeakMap एक खास काम के लिए बना है: ऑब्जेक्ट्स से मेटाडेटा जोड़ना बिना उन ऑब्जेक्ट्स के क्लीनअप होने से रोके। अगर आपको इटरेशन, प्रिमिटिव कीज़ या एंट्रीज़ की गिनती चाहिए, तो आप शायद कोई अलग समस्या सुलझा रहे हैं और आपको सामान्य Map इस्तेमाल करना चाहिए।
ये कहाँ असल में काम आता है
“प्राइवेट डेटा” पैटर्न WeakMap का मूल यूज़ केस था, उस ज़माने में जब JavaScript में #private फ़ील्ड्स नहीं थे। लाइब्रेरीज़ क्लास के बाहर एक WeakMap बनाती थीं और उसका इस्तेमाल ऐसा डेटा स्टोर करने के लिए करती थीं जो इंस्टेंस पर एक्सेस नहीं होना चाहिए।
const privateData = new WeakMap();
class User { constructor(name) { privateData.set(this, { name }); }
getName() { return privateData.get(this).name; }}जब कोई User इंस्टेंस Garbage Collect होता है, प्राइवेट डेटा भी साथ में चला जाता है। किसी क्लीनअप कोड की ज़रूरत नहीं।
मेमोइज़ेशन एक और स्वाभाविक फिट है, ख़ासकर जब आप ऑब्जेक्ट इनपुट्स के आधार पर रिज़ल्ट कैश कर रहे हों, प्रिमिटिव वैल्यूज़ के बजाय। अगर आपका भारी कैलकुलेशन किसी कॉन्फ़िग ऑब्जेक्ट को इनपुट लेता है, तो WeakMap का मतलब है आपको चिंता नहीं कि कैश कॉन्फ़िग्स से ज़्यादा देर तक ज़िंदा रहेगा।
const cache = new WeakMap();
function expensiveCalc(obj) { if (cache.has(obj)) return cache.get(obj);
const result = heavyMath(obj); cache.set(obj, result); return result;}कैश सिर्फ उतनी देर ज़िंदा रहता है जितनी देर कैश किए जा रहे ऑब्जेक्ट्स। जब obj कहीं और रिफ़रेंस नहीं होता, कैश किया रिज़ल्ट और कैश की एंट्री साथ में गायब हो जाते हैं।
कब इस्तेमाल करें
आधुनिक वेब ऐप्स में मेमोरी लीक आमतौर पर पुराने रिफ़रेंसेज़ की वजह से होते हैं — ऐसी चीज़ें जिन्हें क्लीनअप होना चाहिए था। अगर आप कुछ भी लंबे समय तक चलने वाला बना रहे हैं — एक डैशबोर्ड जो पूरा दिन खुला रहता है, एक चैट ऐप जो घंटों चलता है, एक एडमिन पैनल जो कभी रिफ़्रेश नहीं होता — तो आपको सोचना होगा कि पुराने डेटा का क्या होता है।
WeakMap ख़ासतौर पर तब काम आता है जब आप DOM नोड्स, कंपोनेंट इंस्टेंसेज़, या किसी ऐसे ऑब्जेक्ट के साथ डेटा जोड़ रहे हों जिसकी उम्र आप कंट्रोल नहीं करते। अगर आप किसी रिफ़रेंस के आधार पर कुछ स्टोर कर रहे हैं और वो रिफ़रेंस हट सकता है, तो WeakMap क्लीनअप को बहुत आसान बनाता है।
सामान्य Map तभी सही चुनाव है जब आप एक असली कैश इविक्शन पॉलिसी के साथ बना रहे हों, जब आपको एंट्रीज़ पर इटरेट करना हो, जब आप प्रिमिटिव कीज़ इस्तेमाल कर रहे हों, या जब डेटा खुद मायने रखता हो, किसी ऑब्जेक्ट से उसके जुड़ाव से नहीं।
WeakMap की अच्छी बात ये है कि आमतौर पर साफ़ होता है कि आपको इसकी ज़रूरत है। अगर आप क्लीनअप कोड लिख रहे हैं ताकि ऑब्जेक्ट्स डिस्ट्रॉय होने पर Map एंट्रीज़ हटा सकें, तो ये एक संकेत है। अगर आप चिंतित हैं कि मेमोरी अनियंत्रित बढ़ रही है क्योंकि आप समझ नहीं रहे कि कुछ कब डिलीट करें, तो ये भी एक संकेत है।
कभी-कभी सबसे अच्छी फ़ीचर वो होती है जो बिना आपको सोचने पर मजबूर किए सही काम करती है।
संसाधन
- MDN: WeakMap
- MDN: Memory Management
- V8 Blog: Weak References and Finalizers
- JavaScript.info: WeakMap and WeakSet