अलग विंडो मेमोरी लीक

अलग की गई विंडो की वजह से होने वाली मेमोरी लीक को ढूंढना और ठीक करना.

Bartek Nowierski
Bartek Nowierski

JavaScript में मेमोरी लीक क्या है?

मेमोरी लीक का मतलब है कि समय के साथ किसी ऐप्लिकेशन में इस्तेमाल की जाने वाली मेमोरी में अनजाने में बढ़ोतरी हो जाती है. JavaScript में, मेमोरी लीक तब होती है, जब ऑब्जेक्ट की ज़रूरत नहीं होती, लेकिन फ़ंक्शन या अन्य ऑब्जेक्ट अब भी उनका रेफ़रंस देते हैं. इन रेफ़रंस की मदद से, गै़रबेज कलेक्टर को ज़रूरत के मुताबिक ऑब्जेक्ट वापस पाने से रोका जाता है.

गै़रबेज कलेक्टर का काम, उन ऑब्जेक्ट की पहचान करना और उन्हें वापस पाना है जो अब ऐप्लिकेशन से ऐक्सेस नहीं किए जा सकते. यह तब भी काम करता है, जब ऑब्जेक्ट खुद का रेफ़रंस देते हैं या एक-दूसरे का रेफ़रंस देते हैं. जब कोई ऐप्लिकेशन ऑब्जेक्ट के ग्रुप को ऐक्सेस करने के लिए कोई रेफ़रंस नहीं छोड़ता है, तो उसे गै़रबेज कलेक्शन किया जा सकता है.

let A = {};
console.log(A); // local variable reference

let B = {A}; // B.A is a second reference to A

A = null; // unset local variable reference

console.log(B.A); // A can still be referenced by B

B.A = null; // unset B's reference to A

// No references to A are left. It can be garbage collected.

मेमोरी लीक की एक खास क्लास तब होती है, जब कोई ऐप्लिकेशन ऐसे ऑब्जेक्ट का रेफ़रंस देता है जिनका अपना लाइफ़साइकल होता है. जैसे, डीओएम एलिमेंट या पॉप-अप विंडो. ऐसा हो सकता है कि इस तरह के ऑब्जेक्ट, ऐप्लिकेशन के इस्तेमाल के बिना ही इस्तेमाल न किए जाएं. इसका मतलब है कि ऐप्लिकेशन कोड में, किसी ऐसे ऑब्जेक्ट के सिर्फ़ रेफ़रंस बचे हों जिसे ग़ैर-ज़रूरी डेटा के तौर पर हटाया जा सकता है.

अलग की गई विंडो क्या होती है?

नीचे दिए गए उदाहरण में, स्लाइड शो व्यूअर ऐप्लिकेशन में प्रज़ेंटर के नोट वाले पॉप-अप को खोलने और बंद करने के लिए बटन शामिल हैं. मान लें कि कोई उपयोगकर्ता नोट दिखाएं पर क्लिक करता है. इसके बाद, नोट छिपाएं बटन पर क्लिक करने के बजाय, सीधे पॉप-अप विंडो को बंद कर देता है. ऐसे में, notesWindow वैरिएबल में अब भी पॉप-अप का रेफ़रंस मौजूद होता है. इस रेफ़रंस को ऐक्सेस किया जा सकता है, भले ही पॉप-अप का इस्तेमाल अब न किया जा रहा हो.

<button id="show">Show Notes</button>
<button id="hide">Hide Notes</button>
<script type="module">
  let notesWindow;
  document.getElementById('show').onclick = () => {
    notesWindow = window.open('/presenter-notes.html');
  };
  document.getElementById('hide').onclick = () => {
    if (notesWindow) notesWindow.close();
  };
</script>

यह अलग की गई विंडो का उदाहरण है. पॉप-अप विंडो बंद हो गई थी, लेकिन हमारे कोड में इसका रेफ़रंस है. इस वजह से, ब्राउज़र उसे मिटा नहीं पाता और उस मेमोरी को वापस नहीं पाता.

जब कोई पेज नई ब्राउज़र विंडो या टैब बनाने के लिए window.open() को कॉल करता है, तो एक Window ऑब्जेक्ट दिखाया जाता है, जो विंडो या टैब को दिखाता है. ऐसी विंडो बंद होने या उपयोगकर्ता के किसी दूसरी विंडो पर जाने के बाद भी, window.open() से मिले Window ऑब्जेक्ट का इस्तेमाल, उसके बारे में जानकारी ऐक्सेस करने के लिए किया जा सकता है. यह अलग की गई विंडो का एक टाइप है: JavaScript कोड अब भी बंद Window ऑब्जेक्ट की प्रॉपर्टी ऐक्सेस कर सकता है. इसलिए, इसे मेमोरी में रखना ज़रूरी है. अगर विंडो में बहुत ज़्यादा JavaScript ऑब्जेक्ट या iframe शामिल हैं, तो उस मेमोरी को तब तक वापस नहीं पाया जा सकता, जब तक विंडो की प्रॉपर्टी में कोई JavaScript रेफ़रंस नहीं रह जाता.

Chrome DevTools का इस्तेमाल करके यह दिखाया गया है कि विंडो बंद होने के बाद, दस्तावेज़ को कैसे बनाए रखा जा सकता है.

<iframe> एलिमेंट का इस्तेमाल करने पर भी यही समस्या आ सकती है. iframe, नेस्ट की गई उन विंडो की तरह काम करते हैं जिनमें दस्तावेज़ होते हैं. साथ ही, उनकी contentWindow प्रॉपर्टी, Window ऑब्जेक्ट को ऐक्सेस करने की सुविधा देती है. यह वैल्यू, window.open() से मिलने वाली वैल्यू की तरह ही होती है. JavaScript कोड, iframe के contentWindow या contentDocument का रेफ़रंस रख सकता है. भले ही, iframe को DOM से हटा दिया गया हो या उसका यूआरएल बदल गया हो. इससे दस्तावेज़ को ग़ैर-ज़रूरी डेटा के तौर पर हटाने से रोका जा सकता है, क्योंकि उसकी प्रॉपर्टी अब भी ऐक्सेस की जा सकती हैं.

इस इमेज में दिखाया गया है कि इवेंट हैंडलर, iframe को किसी दूसरे यूआरएल पर ले जाने के बाद भी, iframe के दस्तावेज़ को कैसे बनाए रख सकता है.

अगर किसी विंडो या iframe में document का रेफ़रंस, JavaScript से बनाए रखा जाता है, तो उस दस्तावेज़ को मेमोरी में रखा जाएगा. भले ही, उस विंडो या iframe में कोई नया यूआरएल खोला गया हो. यह समस्या तब खास तौर पर परेशानी पैदा कर सकती है, जब उस रेफ़रंस को होल्ड करने वाले JavaScript को पता न चले कि विंडो/फ़्रेम किसी नए यूआरएल पर नेविगेट हो गया है. ऐसा इसलिए, क्योंकि उसे यह पता नहीं होता कि वह दस्तावेज़ को मेमोरी में सेव करने वाला आखिरी रेफ़रंस कब बन गया.

अलग की गई विंडो से मेमोरी लीक कैसे होती है

प्राइमरी पेज के डोमेन पर विंडो और iframe के साथ काम करते समय, आम तौर पर इवेंट को सुनने या दस्तावेज़ की सीमाओं के पार प्रॉपर्टी को ऐक्सेस करने की ज़रूरत होती है. उदाहरण के लिए, इस गाइड की शुरुआत में दिए गए प्रज़ेंटेशन व्यूअर के उदाहरण पर फिर से गौर करें. प्रज़ेंटर के नोट दिखाने के लिए, दर्शक दूसरी विंडो खोलते हैं. स्पीकर नोट विंडो, अगली स्लाइड पर जाने के लिए, click इवेंट को अपनी क्यू के तौर पर सुनती है. अगर उपयोगकर्ता इस नोट वाली विंडो को बंद कर देता है, तो मूल पैरंट विंडो में चल रहे JavaScript के पास अब भी स्पीकर नोट के दस्तावेज़ का पूरा ऐक्सेस होता है:

<button id="notes">Show Presenter Notes</button>
<script type="module">
  let notesWindow;
  function showNotes() {
    notesWindow = window.open('/presenter-notes.html');
    notesWindow.document.addEventListener('click', nextSlide);
  }
  document.getElementById('notes').onclick = showNotes;

  let slide = 1;
  function nextSlide() {
    slide += 1;
    notesWindow.document.title = `Slide  ${slide}`;
  }
  document.body.onclick = nextSlide;
</script>

मान लें कि हमने ऊपर showNotes() से बनाई गई ब्राउज़र विंडो बंद कर दी है. विंडो बंद होने का पता लगाने के लिए, कोई इवेंट हैंडलर नहीं है. इसलिए, हमारे कोड को यह जानकारी नहीं मिल रही है कि उसे दस्तावेज़ के किसी भी रेफ़रंस को हटा देना चाहिए. nextSlide() फ़ंक्शन अब भी "लाइव" है, क्योंकि इसे हमारे मुख्य पेज में क्लिक हैंडलर के तौर पर बाउंड किया गया है. साथ ही, nextSlide में notesWindow का रेफ़रंस होने का मतलब है कि विंडो का अब भी रेफ़रंस दिया गया है और इसे ग़ैर-ज़रूरी डेटा के तौर पर हटाया नहीं जा सकता.

इस इलस्ट्रेशन में दिखाया गया है कि किसी विंडो के रेफ़रंस, उसे बंद होने के बाद भी ग़ैर-ज़रूरी डेटा के तौर पर सेव होने से कैसे रोकते हैं.

ऐसे कई और मामले भी हैं जिनमें रेफ़रंस गलती से सेव हो जाते हैं. इनकी वजह से, अलग की गई विंडो, गै़रबेज कलेक्शन की ज़रूरी शर्तें पूरी नहीं कर पाती हैं:

  • इवेंट हैंडलर, फ़्रेम के अपने टारगेट यूआरएल पर नेविगेट करने से पहले, iframe के शुरुआती दस्तावेज़ पर रजिस्टर किए जा सकते हैं. इस वजह से, अन्य रेफ़रंस हटाने के बाद भी, दस्तावेज़ और iframe के रेफ़रंस बने रहते हैं.

  • किसी विंडो या iframe में लोड किया गया ज़्यादा मेमोरी वाला दस्तावेज़, नए यूआरएल पर जाने के बाद भी गलती से मेमोरी में बना रह सकता है. ऐसा अक्सर तब होता है, जब पैरंट पेज पर दस्तावेज़ के रेफ़रंस सेव रहते हैं, ताकि दर्शक को हटाया जा सके.

  • किसी JavaScript ऑब्जेक्ट को किसी दूसरी विंडो या iframe में पास करते समय, ऑब्जेक्ट की प्रोटोटाइप चेन में उस एनवायरमेंट के रेफ़रंस शामिल होते हैं जिसमें उसे बनाया गया था. इसमें उसे बनाने वाली विंडो भी शामिल होती है. इसका मतलब है कि दूसरी विंडो के ऑब्जेक्ट के रेफ़रंस से बचना उतना ही ज़रूरी है जितना कि खुद विंडो के रेफ़रंस से बचना.

    index.html:

    <script>
      let currentFiles;
      function load(files) {
        // this retains the popup:
        currentFiles = files;
      }
      window.open('upload.html');
    </script>
    

    upload.html:

    <input type="file" id="file" />
    <script>
      file.onchange = () => {
        parent.load(file.files);
      };
    </script>
    

अलग की गई विंडो की वजह से मेमोरी लीक का पता लगाना

मेमोरी लीक का पता लगाना मुश्किल हो सकता है. आम तौर पर, इन समस्याओं को अलग-अलग तरीके से दोहराना मुश्किल होता है. खास तौर पर, जब कई दस्तावेज़ या विंडो शामिल हों. बात को और मुश्किल बनाने के लिए, लीक होने वाले संभावित रेफ़रंस की जांच करने पर, अतिरिक्त रेफ़रंस बन सकते हैं. ये रेफ़रंस, जांच किए गए ऑब्जेक्ट को ग़ैर-ज़रूरी डेटा के तौर पर हटाने से रोकते हैं. इसलिए, ऐसे टूल का इस्तेमाल करना शुरू करें जो इस तरह की संभावना को रोकते हैं.

हीप का स्नैपशॉट लें. इससे, मेमोरी से जुड़ी समस्याओं को डीबग करने में मदद मिलती है. इससे, किसी ऐप्लिकेशन के इस्तेमाल में मौजूद मेमोरी का एक पॉइंट-इन-टाइम व्यू मिलता है. इसमें वे सभी ऑब्जेक्ट शामिल होते हैं जिन्हें बनाया गया है, लेकिन अब तक गै़रबेज इकट्ठा नहीं किया गया है. हीप स्नैपशॉट में ऑब्जेक्ट के बारे में काम की जानकारी होती है. इसमें उनके साइज़ के साथ-साथ, उन वैरिएबल और क्लोज़र की सूची भी शामिल होती है जिनमें उनका रेफ़रंस दिया गया है.

Chrome DevTools में ढेर के स्नैपशॉट का स्क्रीनशॉट, जिसमें बड़े ऑब्जेक्ट को बनाए रखने वाले रेफ़रंस दिख रहे हैं.
हीप स्नैपशॉट, जिसमें बड़े ऑब्जेक्ट को बनाए रखने वाले रेफ़रंस दिखाए गए हैं.

ढेर का स्नैपशॉट रिकॉर्ड करने के लिए, Chrome DevTools में मेमोरी टैब पर जाएं. इसके बाद, उपलब्ध प्रोफ़ाइलिंग टाइप की सूची में से ढेर का स्नैपशॉट चुनें. रिकॉर्डिंग पूरी हो जाने के बाद, खास जानकारी व्यू में, मेमोरी में मौजूद मौजूदा ऑब्जेक्ट दिखते हैं. इन्हें कन्स्ट्रक्टर के हिसाब से ग्रुप किया जाता है.

Chrome DevTools में हीप का स्नैपशॉट लेने का तरीका.

हीप डंप का विश्लेषण करना मुश्किल हो सकता है. साथ ही, डीबग करने के दौरान सही जानकारी ढूंढना भी मुश्किल हो सकता है. इस समस्या को हल करने के लिए, Chromium के इंजीनियर yossik@ और peledni@ ने एक स्टैंडअलोन हीप क्लीनर टूल बनाया है. इसकी मदद से, किसी खास नोड को हाइलाइट किया जा सकता है, जैसे कि अलग की गई विंडो. किसी ट्रेस पर 'हीप क्लीनर' चलाने से, निजी डेटा के रखरखाव के ग्राफ़ से अन्य ग़ैर-ज़रूरी जानकारी हट जाती है. इससे ट्रेस साफ़ हो जाता है और उसे पढ़ना आसान हो जाता है.

प्रोग्राम के हिसाब से मेमोरी को मेज़र करना

हीप स्नैपशॉट से ज़्यादा जानकारी मिलती है. साथ ही, यह पता लगाने में मदद मिलती है कि लीक कहां से हो रही है. हालांकि, हीप स्नैपशॉट लेने की प्रोसेस मैन्युअल होती है. मेमोरी लीक की जांच करने का एक और तरीका है, performance.memory एपीआई से फ़िलहाल इस्तेमाल किए जा रहे JavaScript ढेर का साइज़ पाना:

Chrome DevTools के यूज़र इंटरफ़ेस के सेक्शन का स्क्रीनशॉट.
जब पॉप-अप बनाया जाता है, बंद किया जाता है, और रेफ़रंस हटाया जाता है, तो DevTools में इस्तेमाल किए गए JS हीप साइज़ की जांच की जाती है.

performance.memory API सिर्फ़ JavaScript ढेर के साइज़ की जानकारी देता है. इसका मतलब है कि इसमें पॉप-अप के दस्तावेज़ और संसाधनों के इस्तेमाल की मेमोरी शामिल नहीं होती. पूरी जानकारी पाने के लिए, हमें नए performance.measureUserAgentSpecificMemory() एपीआई का इस्तेमाल करना होगा. फ़िलहाल, इस एपीआई को Chrome में आज़माया जा रहा है.

विंडो के अलग होने से होने वाली समस्याओं से बचने के तरीके

अलग की गई विंडो से मेमोरी लीक होने के दो सबसे सामान्य मामले ये हैं: जब पैरंट दस्तावेज़, बंद किए गए पॉप-अप या हटाए गए iframe के रेफ़रंस को सेव रखता है और जब किसी विंडो या iframe पर अचानक नेविगेट करने की वजह से, इवेंट हैंडलर कभी भी अनरजिस्टर नहीं होते.

उदाहरण: पॉप-अप बंद करना

नीचे दिए गए उदाहरण में, पॉप-अप विंडो को खोलने और बंद करने के लिए दो बटन का इस्तेमाल किया गया है. पॉप-अप बंद करें बटन के काम करने के लिए, खुली हुई पॉप-अप विंडो का रेफ़रंस, वैरिएबल में सेव किया जाता है:

<button id="open">Open Popup</button>
<button id="close">Close Popup</button>
<script>
  let popup;
  open.onclick = () => {
    popup = window.open('/login.html');
  };
  close.onclick = () => {
    popup.close();
  };
</script>

पहली नज़र में, ऐसा लगता है कि ऊपर दिया गया कोड सामान्य गड़बड़ियों से बचता है: पॉप-अप के दस्तावेज़ का कोई रेफ़रंस नहीं रखा जाता और पॉप-अप विंडो पर कोई इवेंट हैंडलर रजिस्टर नहीं किया जाता. हालांकि, पॉप-अप खोलें बटन पर क्लिक करने के बाद, popup वैरिएबल अब खुली हुई विंडो का रेफ़रंस देता है. साथ ही, उस वैरिएबल को पॉप-अप बंद करें बटन के क्लिक हैंडलर के दायरे से ऐक्सेस किया जा सकता है. जब तक popup को फिर से असाइन नहीं किया जाता या क्लिक हैंडलर को हटाया नहीं जाता, तब तक उस हैंडलर के popup के साथ शामिल रेफ़रंस का मतलब है कि उसे ग़ैर-ज़रूरी डेटा के तौर पर हटाया नहीं जा सकता.

समाधान: रेफ़रंस हटाना

किसी दूसरी विंडो या उसके दस्तावेज़ का रेफ़रंस देने वाले वैरिएबल, मेमोरी में सेव रहते हैं. JavaScript में ऑब्जेक्ट हमेशा रेफ़रंस होते हैं. इसलिए, वेरिएबल को नई वैल्यू असाइन करने पर, उनका मूल ऑब्जेक्ट का रेफ़रंस हट जाता है. किसी ऑब्जेक्ट के रेफ़रंस को "अनसेट" करने के लिए, हम उन वैरिएबल को null वैल्यू पर फिर से असाइन कर सकते हैं.

इसे पिछले पॉप-अप उदाहरण पर लागू करने के लिए, हम बंद करने वाले बटन के हैंडलर में बदलाव कर सकते हैं, ताकि पॉप-अप विंडो का रेफ़रंस "अनसेट" हो जाए:

let popup;
open.onclick = () => {
  popup = window.open('/login.html');
};
close.onclick = () => {
  popup.close();
  popup = null;
};

इससे मदद मिलती है, लेकिन open() का इस्तेमाल करके बनाई गई विंडो से जुड़ी एक और समस्या का पता चलता है: अगर उपयोगकर्ता, हमारे कस्टम 'बंद करें' बटन पर क्लिक करने के बजाय, विंडो को बंद कर देता है, तो क्या होगा? इसके अलावा, अगर उपयोगकर्ता हमारी खोली गई विंडो में दूसरी वेबसाइटें ब्राउज़ करना शुरू कर दे, तो क्या होगा? पहले हमें लगता था कि popup रेफ़रंस को बंद करने के बटन पर क्लिक करने पर, उसे अनसेट करना काफ़ी है. हालांकि, जब उपयोगकर्ता विंडो को बंद करने के लिए उस बटन का इस्तेमाल नहीं करते, तब भी मेमोरी लीक होती है. इस समस्या को हल करने के लिए, इन मामलों का पता लगाना ज़रूरी है, ताकि जब भी ये मामले सामने आएं, तो उनसे जुड़े रेफ़रंस को अनसेट किया जा सके.

समाधान: निगरानी करना और डिस्पोज करना

कई मामलों में, विंडो खोलने या फ़्रेम बनाने के लिए ज़िम्मेदार JavaScript के पास, उनके लाइफ़साइकल पर खास कंट्रोल नहीं होता. उपयोगकर्ता पॉप-अप बंद कर सकता है या किसी नए दस्तावेज़ पर नेविगेट करने पर, हो सकता है कि पहले विंडो या फ़्रेम में मौजूद दस्तावेज़ अलग हो जाए. दोनों मामलों में, ब्राउज़र pagehide इवेंट को ट्रिगर करता है, ताकि यह सिग्नल दिया जा सके कि दस्तावेज़ को अनलोड किया जा रहा है.

pagehide इवेंट का इस्तेमाल, बंद विंडो और मौजूदा दस्तावेज़ से नेविगेट करने का पता लगाने के लिए किया जा सकता है. हालांकि, एक अहम बात ध्यान रखें: सभी नई विंडो और iframe में एक खाली दस्तावेज़ होता है. इसके बाद, अगर कोई यूआरएल दिया गया है, तो उस पर असींक्रोनस तरीके से नेविगेट किया जाता है. इस वजह से, विंडो या फ़्रेम बनाने के कुछ समय बाद, टारगेट दस्तावेज़ लोड होने से ठीक पहले, एक शुरुआती pagehide इवेंट ट्रिगर होता है. टारगेट दस्तावेज़ के अनलोड होने पर, हमारे रेफ़रंस क्लीनअप कोड को चलाना ज़रूरी है. इसलिए, हमें इस पहले pagehide इवेंट को अनदेखा करना होगा. ऐसा करने के लिए कई तकनीकें हैं. इनमें से सबसे आसान तरीका यह है कि शुरुआती दस्तावेज़ के about:blank यूआरएल से शुरू होने वाले पेज छिपाने वाले इवेंट को अनदेखा करें. पॉप-अप के उदाहरण में यह ऐसा दिखेगा:

let popup;
open.onclick = () => {
  popup = window.open('/login.html');

  // listen for the popup being closed/exited:
  popup.addEventListener('pagehide', () => {
    // ignore initial event fired on "about:blank":
    if (!popup.location.host) return;

    // remove our reference to the popup window:
    popup = null;
  });
};

ध्यान रखें कि यह तरीका सिर्फ़ उन विंडो और फ़्रेम के लिए काम करता है जिनका ऑरिजिन, उस पैरंट पेज जैसा ही हो जहां हमारा कोड चल रहा है. किसी दूसरे ऑरिजिन से कॉन्टेंट लोड करते समय, सुरक्षा से जुड़ी वजहों से location.host और pagehide, दोनों इवेंट उपलब्ध नहीं होते. आम तौर पर, अन्य ऑरिजिन के रेफ़रंस से बचना सबसे अच्छा होता है. हालांकि, कुछ मामलों में ऐसा करना ज़रूरी हो सकता है. ऐसे में, window.closed या frame.isConnected प्रॉपर्टी को मॉनिटर किया जा सकता है. जब ये प्रॉपर्टी, बंद की गई विंडो या हटाए गए iframe को दिखाने के लिए बदल जाती हैं, तो इनके किसी भी रेफ़रंस को अनसेट करना अच्छा होता है.

let popup = window.open('https://example.com');
let timer = setInterval(() => {
  if (popup.closed) {
    popup = null;
    clearInterval(timer);
  }
}, 1000);

समाधान: WeakRef का इस्तेमाल करना

JavaScript में हाल ही में, ऑब्जेक्ट का रेफ़रंस देने का एक नया तरीका जोड़ा गया है. इसकी मदद से, गै़रबेज कलेक्शन की प्रोसेस शुरू की जा सकती है. इसे WeakRef कहा जाता है. किसी ऑब्जेक्ट के लिए बनाया गया WeakRef, सीधा रेफ़रंस नहीं होता. यह एक अलग ऑब्जेक्ट होता है, जो एक खास .deref() तरीका उपलब्ध कराता है. यह तरीका, तब तक ऑब्जेक्ट का रेफ़रंस दिखाता है, जब तक उसे गै़रबेज-इकट्ठा करने की प्रोसेस के ज़रिए हटाया नहीं जाता. WeakRef की मदद से, किसी विंडो या दस्तावेज़ की मौजूदा वैल्यू को ऐक्सेस किया जा सकता है. साथ ही, उसमें ग़ैर-ज़रूरी डेटा इकट्ठा करने की अनुमति भी दी जा सकती है. pagehide जैसे इवेंट या window.closed जैसी प्रॉपर्टी के जवाब में, मैन्युअल तरीके से सेट किए गए विंडो के रेफ़रंस को बनाए रखने के बजाय, ज़रूरत के हिसाब से विंडो का ऐक्सेस लिया जाता है. विंडो बंद होने पर, उसे ग़ैर-ज़रूरी डेटा के तौर पर हटाया जा सकता है. इस वजह से, .deref() तरीका undefined दिखाना शुरू कर देता है.

<button id="open">Open Popup</button>
<button id="close">Close Popup</button>
<script>
  let popup;
  open.onclick = () => {
    popup = new WeakRef(window.open('/login.html'));
  };
  close.onclick = () => {
    const win = popup.deref();
    if (win) win.close();
  };
</script>

विंडो या दस्तावेज़ों को ऐक्सेस करने के लिए WeakRef का इस्तेमाल करते समय, एक दिलचस्प जानकारी पर ध्यान देना ज़रूरी है. आम तौर पर, विंडो बंद होने या iframe हटने के बाद, रेफ़रंस कुछ समय के लिए उपलब्ध रहता है. ऐसा इसलिए होता है, क्योंकि WeakRef तब तक वैल्यू दिखाता रहता है, जब तक कि उससे जुड़े ऑब्जेक्ट को गै़रबेज इकट्ठा करने की प्रोसेस पूरी नहीं हो जाती. यह प्रोसेस, JavaScript में असिंक्रोनस तरीके से और आम तौर पर, इंतज़ार के समय में होती है. हालांकि, Chrome DevTools के मेमोरी पैनल में, अलग की गई विंडो की जांच करते समय, ढेर के स्नैपशॉट लेने से, असल में गै़रबेज कलेक्शन ट्रिगर होता है और कम रेफ़रंस वाली विंडो को हटा दिया जाता है. यह भी जांचा जा सकता है कि WeakRef के ज़रिए रेफ़र किए गए ऑब्जेक्ट को JavaScript से हटा दिया गया है या नहीं. इसके लिए, deref() के undefined दिखाने पर या नए FinalizationRegistry API का इस्तेमाल करके पता लगाया जा सकता है:

let popup = new WeakRef(window.open('/login.html'));

// Polling deref():
let timer = setInterval(() => {
  if (popup.deref() === undefined) {
    console.log('popup was garbage-collected');
    clearInterval(timer);
  }
}, 20);

// FinalizationRegistry API:
let finalizers = new FinalizationRegistry(() => {
  console.log('popup was garbage-collected');
});
finalizers.register(popup.deref());

समाधान: postMessage पर संपर्क करें

विंडो बंद होने या नेविगेशन के किसी दस्तावेज़ को अनलोड करने का पता लगाने से, हमें हैंडलर हटाने और रेफ़रंस हटाने का तरीका मिलता है. इससे, अलग की गई विंडो को ग़ैर-ज़रूरी डेटा के तौर पर हटाया जा सकता है. हालांकि, ये बदलाव, पेजों के बीच डायरेक्ट कपलिंग की समस्या को ठीक करने के लिए किए गए हैं. यह समस्या कभी-कभी ज़्यादा गंभीर हो सकती है.

एक और बेहतर तरीका उपलब्ध है, जिससे विंडो और दस्तावेज़ों के बीच पुराने रेफ़रंस से बचा जा सकता है: postMessage() तक क्रॉस-दस्तावेज़ कम्यूनिकेशन को सीमित करके, अलगाव बनाएं. प्रज़ेंटर नोट के हमारे मूल उदाहरण पर वापस आकर, nextSlide() जैसे फ़ंक्शन, नोट विंडो को सीधे तौर पर अपडेट करते हैं. इसके लिए, वे नोट विंडो का रेफ़रंस लेते हैं और उसके कॉन्टेंट में बदलाव करते हैं. इसके बजाय, मुख्य पेज postMessage() के ज़रिए, नोट विंडो में ज़रूरी जानकारी को अलग-अलग समय पर और अप्रत्यक्ष तरीके से भेज सकता है.

let updateNotes;
function showNotes() {
  // keep the popup reference in a closure to prevent outside references:
  let win = window.open('/presenter-view.html');
  win.addEventListener('pagehide', () => {
    if (!win || !win.location.host) return; // ignore initial "about:blank"
    win = null;
  });
  // other functions must interact with the popup through this API:
  updateNotes = (data) => {
    if (!win) return;
    win.postMessage(data, location.origin);
  };
  // listen for messages from the notes window:
  addEventListener('message', (event) => {
    if (event.source !== win) return;
    if (event.data[0] === 'nextSlide') nextSlide();
  });
}
let slide = 1;
function nextSlide() {
  slide += 1;
  // if the popup is open, tell it to update without referencing it:
  if (updateNotes) {
    updateNotes(['setSlide', slide]);
  }
}
document.body.onclick = nextSlide;

हालांकि, इसके लिए अब भी ज़रूरी है कि विंडो एक-दूसरे का रेफ़रंस दें, लेकिन कोई भी विंडो, किसी दूसरी विंडो के मौजूदा दस्तावेज़ का रेफ़रंस नहीं रखती. मैसेज पास करने के तरीके से, ऐसे डिज़ाइन बनाने में भी मदद मिलती है जिनमें विंडो के रेफ़रंस एक ही जगह पर रखे जाते हैं. इसका मतलब है कि विंडो बंद करने या किसी दूसरी विंडो पर जाने पर, सिर्फ़ एक रेफ़रंस को अनसेट करना होगा. ऊपर दिए गए उदाहरण में, सिर्फ़ showNotes() के पास नोट विंडो का रेफ़रंस होता है. साथ ही, यह pagehide इवेंट का इस्तेमाल करके यह पक्का करता है कि रेफ़रंस को हटा दिया गया है.

समाधान: noopener का इस्तेमाल करके रेफ़रंस देने से बचें

अगर कोई ऐसी पॉप-अप विंडो खुलती है जिससे आपके पेज को इंटरैक्ट करने या कंट्रोल करने की ज़रूरत नहीं है, तो हो सकता है कि आप कभी भी उस विंडो का रेफ़रंस न पाएं. यह सुविधा खास तौर पर तब फ़ायदेमंद होती है, जब ऐसी विंडो या iframe बनाए जा रहे हों जिनमें किसी दूसरी साइट का कॉन्टेंट लोड किया जाएगा. इन मामलों में, window.open() "noopener" विकल्प स्वीकार करता है. यह विकल्प, एचटीएमएल लिंक के लिए rel="noopener" एट्रिब्यूट की तरह ही काम करता है:

window.open('https://example.com/share', null, 'noopener');

"noopener" विकल्प की वजह से, window.open() null दिखाता है. इससे, पॉप-अप का रेफ़रंस गलती से सेव नहीं हो पाता. इससे पॉप-अप विंडो को अपनी पैरंट विंडो का रेफ़रंस पाने से भी रोका जाता है, क्योंकि window.opener प्रॉपर्टी null होगी.

सुझाव/राय दें या शिकायत करें

उम्मीद है कि इस लेख में दिए गए कुछ सुझावों से, मेमोरी लीक का पता लगाने और उसे ठीक करने में मदद मिलेगी. अगर आपके पास अलग-अलग विंडो को डीबग करने के लिए कोई और तरीका है या इस लेख से आपको अपने ऐप्लिकेशन में लीक का पता लगाने में मदद मिली है, तो हमें बताएं! मुझे Twitter पर @_developit पर ढूंढा जा सकता है.