Temukan dan perbaiki kebocoran memori rumit yang disebabkan oleh jendela yang terputus.
Apa yang dimaksud dengan kebocoran memori di JavaScript?
Kebocoran memori adalah peningkatan jumlah memori yang digunakan oleh aplikasi secara tidak sengaja dari waktu ke waktu. Dalam JavaScript, kebocoran memori terjadi saat objek tidak lagi diperlukan, tetapi masih direferensikan oleh fungsi atau objek lain. Referensi ini mencegah objek yang tidak diperlukan agar tidak diklaim kembali oleh pembersih sampah memori.
Tugas pembersih sampah memori adalah mengidentifikasi dan mengklaim kembali objek yang tidak lagi dapat dijangkau dari aplikasi. Ini dapat diterapkan meskipun objek mereferensikan dirinya sendiri, atau secara siklis mereferensikan satu sama lain–setelah tidak ada lagi referensi yang dapat digunakan aplikasi untuk mengakses sekelompok objek, aplikasi dapat dibersihkan sampah memorinya.
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.
Class kebocoran memori yang sangat rumit terjadi saat aplikasi mereferensikan objek yang memiliki siklus prosesnya sendiri, seperti elemen DOM atau jendela pop-up. Jenis objek ini dapat menjadi tidak digunakan tanpa diketahui aplikasi. Artinya, kode aplikasi mungkin memiliki satu-satunya referensi tersisa ke objek yang seharusnya dapat dibersihkan sampah memori.
Apa itu jendela terpisah?
Dalam contoh berikut, aplikasi penampil slideshow menyertakan tombol untuk membuka dan menutup
pop-up catatan presenter. Bayangkan pengguna mengklik Show Notes, lalu menutup jendela pop-up secara langsung, bukan mengklik tombol Hide Notes, variabel notesWindow
masih menyimpan referensi ke pop-up yang dapat diakses, meskipun pop-up tersebut tidak lagi digunakan.
<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>
Ini adalah contoh jendela terlepas. Jendela pop-up ditutup, tetapi kode kita memiliki referensi yang mencegah browser menghancurkannya dan mengklaim kembali memori tersebut.
Saat halaman memanggil window.open()
untuk membuat tab atau jendela browser baru, objek Window
akan ditampilkan yang
mewakili jendela atau tab. Meskipun jendela tersebut telah ditutup atau pengguna keluar
dari jendela tersebut, objek Window
yang ditampilkan dari window.open()
masih dapat digunakan untuk mengakses informasi
tentang jendela tersebut. Ini adalah salah satu jenis jendela terpisah: karena kode JavaScript masih berpotensi mengakses
properti pada objek Window
yang tertutup, kode tersebut harus disimpan di memori. Jika jendela menyertakan banyak
objek JavaScript atau iframe, memori tersebut tidak dapat diklaim kembali hingga tidak ada
referensi JavaScript yang tersisa ke properti jendela.
Masalah yang sama juga dapat terjadi saat menggunakan elemen <iframe>
. Iframe berperilaku seperti jendela bertingkat
yang berisi dokumen, dan properti contentWindow
-nya memberikan akses ke objek Window
yang ada di dalamnya, seperti nilai yang ditampilkan oleh window.open()
. Kode JavaScript dapat mempertahankan referensi ke
contentWindow
atau contentDocument
iframe meskipun iframe dihapus dari DOM atau URL-nya berubah, yang mencegah dokumen dibersihkan dari sampah karena propertinya masih dapat diakses.
Jika referensi ke document
dalam jendela atau iframe dipertahankan dari JavaScript, dokumen tersebut akan disimpan di memori meskipun jendela atau iframe yang memuatnya membuka URL baru. Hal ini dapat sangat merepotkan saat JavaScript yang menyimpan referensi tersebut tidak mendeteksi
bahwa jendela/frame telah mengarah ke URL baru, karena tidak tahu kapan JavaScript menjadi referensi terakhir
yang menyimpan dokumen dalam memori.
Cara jendela yang terputus menyebabkan kebocoran memori
Saat menggunakan jendela dan iframe di domain yang sama dengan halaman utama, biasanya peristiwa atau akses properti di seluruh batas dokumen akan diproses. Sebagai contoh, mari kita lihat kembali variasi
{i>presentasi penampil presentasi <i}dari awal panduan ini. Penampil akan membuka jendela kedua
untuk menampilkan catatan pembicara. Jendela catatan pembicara memproses peristiwa click
sebagai isyaratnya
untuk berpindah ke slide berikutnya. Jika pengguna menutup jendela catatan ini, JavaScript yang berjalan di
jendela induk asli masih memiliki akses penuh ke dokumen catatan pembicara:
<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>
Bayangkan kita menutup jendela browser yang dibuat oleh showNotes()
di atas. Tidak ada pengendali peristiwa yang mendeteksi bahwa jendela telah ditutup, sehingga tidak ada yang memberi tahu kode kita bahwa jendela tersebut harus membersihkan referensi apa pun ke dokumen. Fungsi nextSlide()
masih "aktif" karena
terikat sebagai pengendali klik di halaman utama, dan fakta bahwa nextSlide
berisi referensi ke
notesWindow
berarti jendela masih direferensikan dan tidak dapat dibersihkan sampah memori.
Ada sejumlah skenario lain saat referensi secara tidak sengaja disimpan sehingga mencegah jendela yang terpisah memenuhi syarat untuk pembersihan sampah memori:
Pengendali peristiwa dapat didaftarkan di dokumen awal iframe sebelum frame membuka URL yang dimaksudkan, sehingga menyebabkan referensi yang tidak disengaja ke dokumen dan iframe tetap ada setelah referensi lain dibersihkan.
Dokumen dengan banyak memori yang dimuat di jendela atau iframe dapat tersimpan secara tidak sengaja dalam memori lama setelah membuka URL baru. Hal ini sering kali disebabkan oleh halaman induk yang mempertahankan referensi ke dokumen agar pemroses dapat dihapus.
Saat meneruskan objek JavaScript ke jendela atau iframe lain, rantai prototipe Objek tersebut menyertakan referensi ke lingkungan pembuatannya, termasuk jendela yang membuatnya. Artinya, menghindari memegang referensi ke objek dari jendela lain sama pentingnya dengan menghindari referensi ke jendela itu sendiri.
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>
Mendeteksi kebocoran memori yang disebabkan oleh jendela yang terputus
Melacak kebocoran memori bisa menjadi hal yang rumit. Membuat reproduksi masalah ini sering kali sulit dilakukan, terutama jika melibatkan beberapa dokumen atau jendela. Untuk membuatnya lebih rumit, memeriksa potensi referensi yang bocor dapat menghasilkan referensi tambahan yang mencegah pembersihan sampah memori objek yang diperiksa. Untuk itu, ada baiknya memulai dengan alat yang secara khusus menghindari timbulnya kemungkinan ini.
Tempat yang tepat untuk memulai proses debug masalah memori adalah dengan mengambil snapshot heap. Fungsi ini memberikan tampilan point-in-time ke dalam memori yang saat ini digunakan oleh aplikasi - semua objek yang telah dibuat, tetapi belum dibersihkan sampah memorinya. Snapshot heap berisi informasi yang berguna tentang objek, termasuk ukurannya serta daftar variabel dan penutupan yang mereferensikannya.
Untuk merekam cuplikan heap, buka tab Memory di Chrome DevTools dan pilih Heap Snapshot dalam daftar jenis pembuatan profil yang tersedia. Setelah perekaman selesai, tampilan Summary akan menampilkan objek saat ini dalam memori, yang dikelompokkan berdasarkan konstruktor.
Menganalisis heap dump dapat menjadi tugas yang berat, dan mungkin cukup sulit untuk menemukan informasi yang tepat sebagai bagian dari proses debug. Untuk membantu menangani hal ini, engineer Chromium yossik@ dan peledni@ mengembangkan alat Heap Cleaner mandiri yang dapat membantu menandai node tertentu, seperti jendela terpisah. Menjalankan Heap Cleaner pada rekaman aktivitas akan menghapus informasi lain yang tidak diperlukan dari grafik retensi, sehingga membuat rekaman aktivitas lebih bersih dan lebih mudah dibaca.
Mengukur memori secara terprogram
Snapshot heap memberikan detail tingkat tinggi dan sangat cocok untuk mencari tahu tempat terjadinya kebocoran,
tetapi pengambilan snapshot heap merupakan proses manual. Cara lain untuk memeriksa kebocoran memori adalah dengan mendapatkan
ukuran heap JavaScript yang saat ini digunakan dari performance.memory
API:
performance.memory
API hanya memberikan informasi tentang ukuran heap JavaScript, yang berarti
bahwa memori yang digunakan oleh dokumen dan resource pop-up tidak disertakan. Untuk mendapatkan gambaran lengkap, kita
harus menggunakan performance.measureUserAgentSpecificMemory()
API baru yang saat ini
sedang diuji coba di Chrome.
Solusi untuk menghindari kebocoran jendela yang terputus
Dua kasus paling umum saat jendela yang dilepas menyebabkan kebocoran memori adalah saat dokumen induk mempertahankan referensi ke pop-up tertutup atau iframe yang dihapus, dan saat navigasi jendela atau iframe yang tidak diinginkan menyebabkan pengendali peristiwa tidak pernah dibatalkan pendaftarannya.
Contoh: Menutup pop-up
Pada contoh berikut, dua tombol digunakan untuk membuka dan menutup jendela pop-up. Agar tombol Close Popup berfungsi, referensi ke jendela pop-up yang terbuka disimpan dalam variabel:
<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>
Secara sekilas, sepertinya kode di atas menghindari kesalahan umum: tidak ada referensi ke dokumen pop-up yang dipertahankan, dan tidak ada pengendali peristiwa yang terdaftar di jendela pop-up. Namun, setelah tombol Open Popup diklik, variabel popup
sekarang akan mereferensikan jendela yang terbuka, dan variabel tersebut dapat diakses dari cakupan pengendali klik tombol Close Popup. Kecuali jika popup
ditetapkan ulang atau pengendali klik dihapus, referensi tertutup pengendali ke popup
berarti tidak dapat
dibersihkan sampah memori.
Solusi: Batalkan penetapan referensi
Variabel yang mereferensikan jendela lain atau dokumennya menyebabkannya dipertahankan di dalam memori. Karena objek dalam JavaScript selalu merupakan referensi, menetapkan nilai baru ke variabel akan menghapus referensinya ke objek asli. Untuk "membatalkan penetapan" referensi ke suatu objek, kita dapat menetapkan ulang variabel tersebut ke nilai null
.
Dengan menerapkan ini ke contoh pop-up sebelumnya, kita dapat memodifikasi pengendali tombol tutup untuk membuatnya "membatalkan penetapan" referensinya ke jendela pop-up:
let popup;
open.onclick = () => {
popup = window.open('/login.html');
};
close.onclick = () => {
popup.close();
popup = null;
};
Cara ini membantu, tetapi menampilkan masalah lebih lanjut khusus untuk jendela yang dibuat menggunakan open()
: bagaimana jika
pengguna menutup jendela, bukan mengklik tombol tutup khusus? Lebih jauh lagi, bagaimana jika pengguna mulai
menjelajahi situs web lain di jendela yang kita buka? Meskipun awalnya tampaknya cukup untuk
membatalkan penetapan referensi popup
saat mengklik tombol tutup, masih ada kebocoran memori ketika pengguna
tidak menggunakan tombol tertentu untuk menutup jendela. Untuk menyelesaikan masalah ini, Anda harus mendeteksi kasus ini
agar dapat mengganggu referensi yang masih ada saat terjadi.
Solusi: Pantau dan buang
Dalam banyak situasi, JavaScript yang bertanggung jawab untuk membuka jendela atau membuat frame tidak memiliki
kontrol eksklusif atas siklus prosesnya. Pop-up dapat ditutup oleh pengguna, atau navigasi ke dokumen
baru dapat menyebabkan dokumen yang sebelumnya terdapat dalam jendela atau frame terlepas. Dalam
kedua kasus, browser mengaktifkan peristiwa pagehide
untuk menandakan bahwa dokumen sedang dihapus muatannya.
Peristiwa pagehide
dapat digunakan untuk mendeteksi jendela yang ditutup dan navigasi keluar dari dokumen
saat ini. Namun, ada satu peringatan penting: semua jendela dan iframe yang baru dibuat berisi dokumen kosong, lalu membuka URL yang diberikan secara asinkron jika disediakan. Akibatnya, peristiwa
pagehide
awal diaktifkan sesaat setelah membuat jendela atau frame, tepat sebelum dokumen
target dimuat. Karena kode pembersihan referensi harus dijalankan saat dokumen target dihapus muatannya, kita harus mengabaikan peristiwa pagehide
pertama ini. Ada sejumlah teknik untuk melakukannya, yang paling sederhana adalah mengabaikan peristiwa pagehide yang berasal dari URL about:blank
dokumen awal. Berikut adalah tampilannya dalam contoh pop-up:
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;
});
};
Penting untuk diperhatikan bahwa teknik ini hanya berfungsi untuk jendela dan frame yang memiliki origin yang efektif
yang sama dengan halaman induk tempat kode kita dijalankan. Saat memuat konten dari origin
yang berbeda, baik peristiwa location.host
maupun pagehide
tidak tersedia karena alasan keamanan. Meskipun
secara umum sebaiknya hindari menyimpan referensi ke asal lain, dalam kasus yang jarang terjadi saat hal ini
diperlukan, Anda dapat memantau properti window.closed
atau frame.isConnected
. Jika properti ini berubah untuk menunjukkan jendela yang tertutup atau iframe yang dihapus, sebaiknya batalkan penetapan referensi ke properti tersebut.
let popup = window.open('https://example.com');
let timer = setInterval(() => {
if (popup.closed) {
popup = null;
clearInterval(timer);
}
}, 1000);
Solusi: Gunakan WeakRef
JavaScript baru-baru ini mendapatkan dukungan untuk cara baru mereferensikan objek yang memungkinkan pembersihan sampah memori berlangsung, yang disebut WeakRef
. WeakRef
yang dibuat untuk suatu objek bukanlah referensi
langsung, melainkan objek terpisah yang menyediakan metode .deref()
khusus yang menampilkan
referensi ke objek tersebut selama objek tersebut belum dibersihkan sampah memorinya. Dengan WeakRef
, Anda
dapat mengakses nilai jendela atau dokumen saat ini sambil tetap mengizinkannya untuk dibersihkan. Alih-alih mempertahankan referensi ke jendela yang harus dibatalkan penetapannya secara manual sebagai respons
terhadap peristiwa seperti pagehide
atau properti seperti window.closed
, akses ke jendela diperoleh
sesuai kebutuhan. Saat jendela ditutup, sampah dapat dibersihkan, yang menyebabkan metode .deref()
mulai menampilkan 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>
Satu detail menarik yang perlu dipertimbangkan saat menggunakan WeakRef
untuk mengakses jendela atau dokumen adalah bahwa
referensi umumnya tetap tersedia dalam waktu singkat setelah jendela ditutup atau
iframe dihapus. Ini karena WeakRef
terus menampilkan nilai hingga objek terkaitnya
telah dibersihkan sampah memorinya, yang terjadi secara asinkron di JavaScript dan umumnya selama waktu
tidak ada aktivitas. Untungnya, saat memeriksa jendela yang terputus di panel Memory Chrome DevTools, mengambil
snapshot heap sebenarnya akan memicu pembersihan sampah memori dan membuang jendela yang direferensikan dengan lemah. Anda juga dapat memeriksa apakah objek yang direferensikan melalui WeakRef
telah dibuang dari JavaScript, baik dengan mendeteksi saat deref()
menampilkan undefined
atau menggunakan FinalizationRegistry
API yang baru:
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());
Solusi: Berkomunikasi melalui postMessage
Mendeteksi saat jendela ditutup atau navigasi menghapus muatan dokumen memberi kita cara untuk menghapus pengendali dan membatalkan penetapan referensi sehingga jendela yang dilepas dapat dibersihkan sampah memori. Namun, perubahan ini adalah perbaikan khusus untuk hal yang terkadang menjadi masalah yang lebih mendasar: pengaitan langsung antar-halaman.
Pendekatan alternatif yang lebih menyeluruh tersedia untuk menghindari referensi tidak berlaku antara jendela dan
dokumen: membuat pemisahan dengan membatasi komunikasi lintas dokumen ke
postMessage()
. Dengan mengingat kembali contoh catatan presenter asli kita, fungsi
seperti nextSlide()
memperbarui jendela catatan secara langsung dengan mereferensikannya dan
memanipulasi kontennya. Sebagai gantinya, halaman utama dapat meneruskan informasi yang diperlukan ke jendela catatan
secara asinkron dan tidak langsung melalui 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;
Meskipun hal ini masih mengharuskan jendela untuk saling mereferensikan, tidak ada yang mempertahankan referensi ke
dokumen saat ini dari jendela lain. Pendekatan penerusan pesan juga mendorong desain di mana
referensi jendela disimpan di satu tempat, yang berarti hanya satu referensi yang perlu dibatalkan penetapannya saat
jendela ditutup atau keluar. Dalam contoh di atas, hanya showNotes()
yang mempertahankan referensi ke jendela catatan, dan menggunakan peristiwa pagehide
untuk memastikan bahwa referensi dibersihkan.
Solusi: Hindari referensi menggunakan noopener
Jika jendela pop-up terbuka sehingga halaman Anda tidak perlu melakukan komunikasi atau mengontrolnya,
Anda mungkin dapat menghindari referensi ke jendela tersebut. Hal ini sangat berguna
saat membuat jendela atau iframe yang akan memuat konten dari situs lain. Untuk kasus ini,
window.open()
menerima opsi "noopener"
yang berfungsi seperti
atribut rel="noopener"
untuk link HTML:
window.open('https://example.com/share', null, 'noopener');
Opsi "noopener"
menyebabkan window.open()
menampilkan null
, sehingga tidak mungkin
menyimpan referensi ke pop-up secara tidak sengaja. Hal ini juga mencegah jendela pop-up mendapatkan
referensi ke jendela induknya, karena properti window.opener
akan menjadi null
.
Masukan
Semoga beberapa saran dalam artikel ini membantu Anda menemukan dan memperbaiki kebocoran memori. Jika Anda memiliki teknik lain untuk men-debug jendela yang terpisah atau artikel ini yang membantu mengungkap kebocoran di aplikasi, saya ingin mengetahuinya. Anda dapat menemukan saya di Twitter @_developit.