หน่วยความจำหน้าต่างที่ปลดออกรั่วไหล

ค้นหาและแก้ไขการรั่วไหลของหน่วยความจำที่ยุ่งยากซึ่งเกิดจากหน้าต่างที่ถอดออก

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.

การรั่วไหลของหน่วยความจำที่มีความซับซ้อนเป็นพิเศษจะเกิดขึ้นเมื่อแอปพลิเคชันอ้างอิงออบเจ็กต์ที่มีวงจรชีวิตของตนเอง เช่น องค์ประกอบ DOM หรือหน้าต่างป๊อปอัป มีโอกาสที่ออบเจ็กต์ประเภทนี้อาจไม่มีการใช้งานโดยที่แอปพลิเคชันไม่ทราบ ซึ่งหมายความว่าโค้ดของแอปพลิเคชันอาจมีการอ้างอิงไปยังออบเจ็กต์ที่อาจเป็นขยะที่รวบรวมได้เพียงอย่างเดียวเท่านั้น

หน้าต่างที่ถูกแยกออกคืออะไร

ในตัวอย่างต่อไปนี้ แอปพลิเคชันดูภาพสไลด์จะมีปุ่มสำหรับเปิดและปิดป๊อปอัปบันทึกของผู้นำเสนอ สมมติว่าผู้ใช้คลิกแสดงบันทึก แล้วปิดหน้าต่างป๊อปอัปโดยตรงแทนที่จะคลิกปุ่มซ่อนบันทึก ตัวแปร 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 ที่ส่งคืนจาก window.open() จะยังคงสามารถใช้เพื่อเข้าถึงข้อมูลเกี่ยวกับหน้าต่างนั้นได้ นี่เป็นหน้าต่างที่แยกออกมาประเภทหนึ่ง: เนื่องจากโค้ด JavaScript ยังมีโอกาสเข้าถึงพร็อพเพอร์ตี้ในออบเจ็กต์ Window ที่ปิดอยู่ จึงต้องเก็บไว้ในหน่วยความจำ หากหน้าต่างมีออบเจ็กต์ JavaScript หรือ iframe จำนวนมาก หน่วยความจำนั้นจะไม่สามารถเรียกคืนได้จนกว่าจะไม่มีการอ้างอิง JavaScript ที่เหลืออยู่ไปยังคุณสมบัติของหน้าต่าง

การใช้เครื่องมือสำหรับนักพัฒนาเว็บใน Chrome เพื่อสาธิตวิธีเก็บรักษาเอกสารหลังจากปิดหน้าต่าง

ปัญหาเดียวกันนี้ยังอาจเกิดขึ้นได้เมื่อใช้องค์ประกอบ <iframe> iframe จะทำงานเหมือนหน้าต่างที่ซ้อนกันที่มีเอกสาร และพร็อพเพอร์ตี้ contentWindow ของ iframe จะให้สิทธิ์เข้าถึงออบเจ็กต์ Window ที่มีอยู่ ซึ่งเหมือนกับค่าที่ window.open() แสดงผล โค้ด JavaScript จะเก็บการอ้างอิงไปยัง contentWindow หรือ contentDocument ของ iframe ได้แม้ว่าจะนำ iframe ออกจาก DOM หรือการเปลี่ยนแปลง URL แล้ว ซึ่งทำให้ระบบเก็บเอกสารขยะเนื่องจากยังเข้าถึงพร็อพเพอร์ตี้ได้อยู่

การสาธิตวิธีที่เครื่องจัดการเหตุการณ์เก็บรักษาเอกสารของ iframe ได้แม้ว่าจะไปยัง URL อื่นใน iframe แล้วก็ตาม

ในกรณีที่การอ้างอิงไปยัง document ภายในหน้าต่างหรือ iframe ถูกเก็บจาก JavaScript เอกสารนั้นจะถูกเก็บไว้ในหน่วยความจำแม้ว่าหน้าต่างหรือ iframe นั้นจะไปที่ URL ใหม่ก็ตาม ปัญหานี้จะสร้างปัญหาอย่างมากเมื่อ JavaScript ที่ระงับการอ้างอิงนั้นตรวจไม่พบว่าหน้าต่าง/เฟรมได้ไปยัง URL ใหม่ เนื่องจากไม่ทราบว่าเมื่อใดที่ JavaScript จะกลายเป็นข้อมูลอ้างอิงสุดท้ายที่เก็บเอกสารไว้ในหน่วยความจำ

หน้าต่างที่ถอดแยกทำให้หน่วยความจำรั่วไหลได้อย่างไร

เมื่อทำงานกับหน้าต่างและ iframe ในโดเมนเดียวกับหน้าหลัก จึงเป็นเรื่องปกติที่จะรับฟังเหตุการณ์หรือเข้าถึงพร็อพเพอร์ตี้ข้ามขอบเขตของเอกสาร ตัวอย่างเช่น เรามาดูหน้ารูปแบบใหม่ ในตัวอย่างโปรแกรมดูงานนำเสนอในช่วงต้นของคู่มือนี้กัน ผู้ดูจะเปิดหน้าต่างที่ 2 สำหรับแสดงบันทึกของผู้บรรยาย หน้าต่างบันทึกของผู้บรรยายจะคอยฟัง 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 ก่อนที่เฟรมจะไปยัง URL ที่ต้องการ ส่งผลให้มีการอ้างอิงเอกสารโดยไม่ตั้งใจและ iframe จะยังคงอยู่หลังจากลบการอ้างอิงอื่นๆ แล้ว

  • เอกสารที่มีหน่วยความจำจำนวนมากซึ่งโหลดในหน้าต่างหรือ iframe อาจเก็บไว้ในหน่วยความจำโดยไม่ได้ตั้งใจเป็นเวลานานหลังจากไปยัง URL ใหม่ ปัญหานี้มักเกิดจากการที่หน้าหลักยังคงอ้างอิงถึงเอกสารดังกล่าวไว้เพื่อให้นำ Listener ออก

  • เมื่อส่งออบเจ็กต์ 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 ซึ่งแสดงข้อมูลอ้างอิงที่เก็บรักษาออบเจ็กต์ขนาดใหญ่ไว้
ฮีพสแนปชอตที่แสดงการอ้างอิงที่มีออบเจ็กต์ขนาดใหญ่

หากต้องการบันทึกสแนปชอตฮีป ให้ไปที่แท็บหน่วยความจำในเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome แล้วเลือกสแนปชอตฮีปในรายการประเภทการสร้างโปรไฟล์ที่ใช้ได้ เมื่อบันทึกเสร็จสิ้นแล้ว มุมมองสรุปจะแสดงออบเจ็กต์ปัจจุบันในหน่วยความจำโดยจัดกลุ่มตามตัวสร้าง

การสาธิตการสร้างฮีพสแนปชอตใน Chrome DevTools

การวิเคราะห์ฮีพดัมพ์อาจเป็นงานที่ยุ่งยาก และการหาข้อมูลที่ถูกต้องเพื่อใช้ในการแก้ไขข้อบกพร่องก็ค่อนข้างยาก วิศวกร Chromium yossik@ และ peledni@ ได้พัฒนาเครื่องมือ Heap Cleaner แบบสแตนด์อโลนที่สามารถช่วยไฮไลต์โหนดที่เฉพาะเจาะจง เช่น หน้าต่างที่แยกออกมา การเรียกใช้ฮีป Cleaner ในการติดตามจะนำข้อมูลที่ไม่จำเป็นอื่นๆ ออกจากกราฟการเก็บรักษา ซึ่งจะทำให้การติดตามดูสะอาดตาและอ่านง่ายขึ้นมาก

วัดหน่วยความจำแบบเป็นโปรแกรม

ฮีปสแนปชอตมีรายละเอียดในระดับสูงและเหมาะสำหรับการระบุตำแหน่งที่เกิดการรั่วไหล แต่การบันทึกฮีปสแนปชอตเป็นกระบวนการที่ต้องดำเนินการเอง อีกวิธีหนึ่งในการตรวจสอบการรั่วไหลของหน่วยความจำคือการรับขนาดฮีป JavaScript ที่ใช้อยู่ในปัจจุบันจาก performance.memory API โดยทำดังนี้

ภาพหน้าจอของส่วนของอินเทอร์เฟซผู้ใช้ Chrome DevTools
การตรวจสอบขนาดฮีป JS ที่ใช้ในเครื่องมือสำหรับนักพัฒนาเว็บเมื่อมีการสร้าง ปิด และไม่มีการอ้างอิง

performance.memory API จะให้ข้อมูลเกี่ยวกับขนาดฮีปของ JavaScript เท่านั้น ซึ่งหมายความว่าไม่รวมหน่วยความจำที่เอกสารและทรัพยากรของป๊อปอัปใช้ เพื่อให้เห็นภาพทั้งหมด เราต้องใช้ performance.measureUserAgentSpecificMemory() API ใหม่ที่กำลังทดลองใช้ใน Chrome

วิธีแก้ปัญหาสำหรับหน้าต่างรั่วซึม

กรณีที่พบบ่อยที่สุด 2 กรณีที่หน้าต่างที่แยกออกมาทำให้หน่วยความจำรั่วไหลคือเมื่อเอกสารหลักเก็บการอ้างอิงไปยังป๊อปอัปที่ปิดไปแล้วหรือ iframe ที่ถูกนำออก และเมื่อการนำทางที่ไม่คาดคิดของหน้าต่างหรือ iframe ทำให้ตัวแฮนเดิลเหตุการณ์ไม่ถูกยกเลิกการลงทะเบียน

ตัวอย่าง: การปิดป๊อปอัป

ในตัวอย่างต่อไปนี้ จะใช้ปุ่ม 2 ปุ่มในการเปิดและปิดหน้าต่างป๊อปอัป ระบบจะจัดเก็บการอ้างอิงไปยังหน้าต่างป๊อปอัปที่เปิดอยู่ไว้ในตัวแปร เพื่อให้ปุ่มปิดป๊อปอัปทำงานได้

<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 ที่มีหน้าที่เปิดหน้าต่างหรือสร้างเฟรมก็ไม่ได้ควบคุมอายุการใช้งานของ JavaScript แต่เพียงผู้เดียว ผู้ใช้สามารถปิดป๊อปอัปได้ หรือการไปยังเอกสารใหม่อาจทำให้เอกสารที่ก่อนหน้านี้อยู่ในหน้าต่างหรือเฟรมถูกถอดออก ในทั้ง 2 กรณี เบราว์เซอร์จะเริ่มการทำงานของเหตุการณ์ pagehide เพื่อส่งสัญญาณว่ามีการยกเลิกการโหลดเอกสาร

คุณใช้เหตุการณ์ pagehide เพื่อตรวจหาหน้าต่างที่ปิดอยู่และออกจากเอกสารปัจจุบันได้ อย่างไรก็ตาม มีข้อควรระวังที่สำคัญอย่างหนึ่งคือ หน้าต่างและ iframe ที่สร้างขึ้นใหม่ทั้งหมดจะมีเอกสารเปล่า แล้วไปยัง URL ที่ให้มาแบบอะซิงโครนัส หากมี ด้วยเหตุนี้ เหตุการณ์ pagehide เริ่มต้นจึงเริ่มทำงานไม่นานหลังจากที่สร้างหน้าต่างหรือเฟรมก่อนที่เอกสารเป้าหมายจะโหลด เนื่องจากโค้ดการล้างข้อมูลข้อมูลอ้างอิงของเราจะต้องทำงานเมื่อมีการยกเลิกการโหลดเอกสาร target เราจึงจำเป็นต้องละเว้นเหตุการณ์ pagehide แรกนี้ วิธีดังกล่าวมีเทคนิคหลายวิธี วิธีที่ง่ายที่สุดคือละเว้นเหตุการณ์ซ่อนหน้าที่มาจาก URL 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" สำหรับลิงก์ HTML

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

ตัวเลือก "noopener" จะทำให้ window.open() แสดงผล null ทำให้ไม่สามารถจัดเก็บการอ้างอิงไปยังป๊อปอัปโดยไม่ได้ตั้งใจได้ และยังป้องกันไม่ให้หน้าต่างป๊อปอัปได้รับการอ้างอิงไปยังหน้าต่างระดับบนสุดด้วย เนื่องจากพร็อพเพอร์ตี้ window.opener จะเป็น null

ความคิดเห็น

เราหวังว่าคำแนะนำบางส่วนในบทความนี้จะช่วยคุณค้นหาและแก้ไขการรั่วไหลของหน่วยความจำได้ หากคุณมีเทคนิคอื่นในการแก้ไขข้อบกพร่องของหน้าต่างที่แยกออกมาหรือบทความนี้ช่วยเผยรอยรั่วในแอป ก็คงจะรู้ดี พบกันได้ที่ Twitter @_developit