Menemukan dan memperbaiki kebocoran memori yang rumit yang disebabkan oleh jendela yang terlepas.
Apa yang dimaksud dengan kebocoran memori di JavaScript?
Kebocoran memori adalah peningkatan jumlah memori yang tidak disengaja yang digunakan oleh aplikasi dari waktu ke waktu. Di JavaScript, kebocoran memori terjadi saat objek tidak lagi diperlukan, tetapi masih direferensikan oleh fungsi atau objek lainnya. Referensi ini mencegah objek yang tidak diperlukan diambil kembali oleh pembersih sampah memori.
Tugas pembersih sampah memori adalah mengidentifikasi dan mengklaim kembali objek yang tidak dapat dijangkau lagi dari aplikasi. Hal ini berfungsi bahkan saat objek mereferensikan dirinya sendiri, atau saling mereferensikan secara siklus–setelah tidak ada referensi yang tersisa yang dapat digunakan aplikasi untuk mengakses sekelompok objek, objek tersebut 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.
Kelas 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 sepengetahuan aplikasi, yang berarti kode aplikasi mungkin memiliki satu-satunya referensi yang tersisa ke objek yang dapat di-garbage collection.
Apa yang dimaksud dengan jendela terpisah?
Pada contoh berikut, aplikasi penampil slide menyertakan tombol untuk membuka dan menutup
pop-up catatan presenter. Bayangkan pengguna mengklik Show Notes, lalu langsung menutup jendela pop-up,
bukan mengklik tombol Hide Notes–variabel notesWindow
masih menyimpan referensi
ke pop-up yang dapat diakses, meskipun pop-up 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 yang terpisah. Jendela pop-up ditutup, tetapi kode kami memiliki referensi ke jendela tersebut yang mencegah browser menghancurkannya dan mengambil kembali memori tersebut.
Saat halaman memanggil window.open()
untuk membuat jendela atau tab browser baru, objek
Window
akan ditampilkan yang
mewakili jendela atau tab. Bahkan setelah jendela tersebut ditutup atau pengguna menutupnya, objek Window
yang ditampilkan dari window.open()
masih dapat digunakan untuk mengakses informasi tentangnya. Ini adalah salah satu jenis jendela terpisah: karena kode JavaScript masih berpotensi mengakses
properti pada objek Window
yang tertutup, kode tersebut harus disimpan dalam memori. Jika jendela menyertakan banyak
objek JavaScript atau iframe, memori tersebut tidak dapat diambil kembali hingga tidak ada lagi
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, seperti nilai yang ditampilkan oleh window.open()
. Kode JavaScript dapat menyimpan referensi ke
contentWindow
atau contentDocument
iframe meskipun iframe dihapus dari DOM atau URL-nya
berubah, yang mencegah dokumen dikumpulkan sampahnya karena propertinya masih dapat
diakses.
Jika referensi ke document
dalam jendela atau iframe dipertahankan dari JavaScript, dokumen tersebut akan disimpan dalam memori meskipun jendela atau iframe yang berisinya membuka URL baru. Hal ini dapat sangat merepotkan jika JavaScript yang menyimpan referensi tersebut tidak mendeteksi bahwa jendela/bingkai telah membuka URL baru, karena tidak tahu kapan menjadi referensi terakhir yang menyimpan dokumen dalam memori.
Cara jendela yang terlepas menyebabkan kebocoran memori
Saat menggunakan jendela dan iframe di domain yang sama dengan halaman utama, biasanya Anda memproses peristiwa atau mengakses properti di seluruh batas dokumen. Misalnya, mari kita lihat kembali variasi
pada contoh penampil presentasi dari awal panduan ini. Penampil akan membuka jendela kedua untuk menampilkan catatan pembicara. Jendela catatan pembicara memproses peristiwa click
sebagai isyarat 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 memproses untuk mendeteksi bahwa jendela telah ditutup, sehingga tidak ada yang memberi tahu kode kita bahwa kode tersebut harus membersihkan referensi apa pun ke dokumen. Fungsi nextSlide()
masih "aktif" karena
diikat sebagai pengendali klik di halaman utama, dan fakta bahwa nextSlide
berisi referensi ke
notesWindow
berarti jendela masih direferensikan dan tidak dapat dikumpulkan sampahnya.
Ada sejumlah skenario lain saat referensi tidak sengaja dipertahankan yang mencegah jendela terpisah memenuhi syarat untuk pengumpulan sampah:
Pengendali peristiwa dapat didaftarkan di dokumen awal iframe sebelum frame membuka URL yang diinginkan, sehingga menyebabkan referensi yang tidak disengaja ke dokumen dan iframe tetap ada setelah referensi lain dibersihkan.
Dokumen yang berat memorinya dimuat di jendela atau iframe dapat secara tidak sengaja disimpan dalam memori lama setelah membuka URL baru. Hal ini sering kali disebabkan oleh halaman induk yang mempertahankan referensi ke dokumen untuk memungkinkan penghapusan pemroses.
Saat meneruskan objek JavaScript ke jendela atau iframe lain, rantai prototipe Objek menyertakan referensi ke lingkungan tempat objek dibuat, termasuk jendela yang membuatnya. Artinya, sama pentingnya untuk menghindari menyimpan referensi ke objek dari jendela lain seperti menghindari menyimpan 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 terlepas
Melacak kebocoran memori bisa jadi sulit. Sering kali sulit untuk membuat reproduksi terpisah dari masalah ini, terutama jika beberapa dokumen atau jendela terlibat. Untuk mempersulit hal-hal, memeriksa potensi kebocoran referensi dapat berakhir dengan membuat referensi tambahan yang mencegah objek yang diperiksa dari pengumpulan sampah. Untuk itu, sebaiknya mulai dengan alat yang secara khusus menghindari kemungkinan ini.
Tempat yang tepat untuk mulai men-debug masalah memori adalah dengan mengambil snapshot heap. Hal ini memberikan tampilan titik waktu ke memori yang saat ini digunakan oleh aplikasi - semua objek yang telah dibuat tetapi belum di-garbage collection. Snapshot heap berisi informasi yang berguna tentang objek, termasuk ukurannya dan daftar variabel dan penutupan yang mereferensikannya.
Untuk merekam snapshot heap, buka tab Memory di Chrome DevTools, lalu pilih Heap Snapshot dalam daftar jenis pembuatan profil yang tersedia. Setelah perekaman selesai, tampilan Ringkasan akan menampilkan objek saat ini dalam memori, yang dikelompokkan menurut konstruktor.
Menganalisis dump heap bisa menjadi tugas yang berat, dan cukup sulit untuk menemukan informasi yang tepat sebagai bagian dari proses debug. Untuk membantu hal ini, engineer Chromium yossik@ dan peledni@ mengembangkan alat Pembersih Heap 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 rekaman aktivitas menjadi lebih bersih dan jauh lebih mudah dibaca.
Mengukur memori secara terprogram
Snapshot heap memberikan tingkat detail yang tinggi dan sangat cocok untuk mengetahui tempat kebocoran terjadi,
tetapi mengambil snapshot heap adalah 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
tidak menyertakan memori yang digunakan oleh dokumen dan resource pop-up. 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 terlepas
Dua kasus paling umum saat jendela yang terpisah menyebabkan kebocoran memori adalah saat dokumen induk mempertahankan referensi ke pop-up yang ditutup atau iframe yang dihapus, dan saat navigasi jendela atau iframe yang tidak terduga menyebabkan pengendali peristiwa tidak pernah dihapus pendaftarannya.
Contoh: Menutup pop-up
Dalam contoh berikut, dua tombol digunakan untuk membuka dan menutup jendela pop-up. Agar tombol Close Popup berfungsi, referensi ke jendela pop-up yang dibuka 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>
Sekilas, kode di atas tampaknya 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
kini mereferensikan jendela yang terbuka, dan variabel tersebut dapat diakses dari cakupan pengendali klik tombol Close Popup. Kecuali jika popup
diatribusikan ulang atau pengendali klik dihapus, referensi yang disertakan pengendali tersebut ke popup
berarti tidak dapat
di-garbage collection.
Solusi: Hapus penetapan referensi
Variabel yang mereferensikan jendela lain atau dokumennya menyebabkan variabel tersebut dipertahankan dalam memori. Karena
objek dalam JavaScript selalu merupakan referensi, menetapkan nilai baru ke variabel akan menghapus
referensinya ke objek asli. Untuk "menghapus" referensi ke objek, kita dapat menetapkan ulang variabel tersebut
ke nilai null
.
Dengan menerapkannya ke contoh pop-up sebelumnya, kita dapat mengubah pengendali tombol tutup agar "tidak menetapkan" referensinya ke jendela pop-up:
let popup;
open.onclick = () => {
popup = window.open('/login.html');
};
close.onclick = () => {
popup.close();
popup = null;
};
Hal ini membantu, tetapi mengungkapkan masalah lebih lanjut khusus untuk jendela yang dibuat menggunakan open()
: bagaimana jika
pengguna menutup jendela, bukan mengklik tombol tutup kustom? Lebih jauh lagi, bagaimana jika pengguna
mulai menjelajahi situs lain di jendela yang kita buka? Meskipun awalnya tampaknya cukup untuk
menghapus setelan referensi popup
saat mengklik tombol tutup, masih ada kebocoran memori saat pengguna
tidak menggunakan tombol tertentu untuk menutup jendela. Untuk mengatasinya, Anda harus mendeteksi kasus ini
agar dapat menetapkan ulang referensi yang tertinggal saat terjadi.
Solusi: Memantau dan membuang
Dalam banyak situasi, JavaScript yang bertanggung jawab untuk membuka jendela atau membuat bingkai tidak memiliki
kontrol eksklusif atas siklus prosesnya. Pop-up dapat ditutup oleh pengguna, atau navigasi ke dokumen baru dapat menyebabkan dokumen yang sebelumnya berada dalam jendela atau bingkai menjadi terlepas. Dalam
kedua kasus tersebut, browser akan memicu peristiwa pagehide
untuk menandakan bahwa dokumen sedang di-unload.
Peristiwa pagehide
dapat digunakan untuk mendeteksi jendela yang ditutup dan navigasi dari dokumen
saat ini. Namun, ada satu peringatan penting: semua jendela dan iframe yang baru dibuat berisi dokumen kosong, lalu secara asinkron membuka URL yang diberikan jika disediakan. Akibatnya, peristiwa
pagehide
awal diaktifkan segera setelah membuat jendela atau bingkai, tepat sebelum dokumen
target dimuat. Karena kode pembersihan referensi perlu dijalankan saat dokumen target
di-unload, 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 tampilannya di 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 bingkai yang memiliki origin efektif
yang sama dengan halaman induk tempat kode kita berjalan. Saat memuat konten dari origin
yang berbeda, peristiwa location.host
dan pagehide
tidak tersedia karena alasan keamanan. Meskipun
sebaiknya hindari menyimpan referensi ke origin lain, dalam kasus yang jarang terjadi, jika hal ini
diperlukan, Anda dapat memantau properti window.closed
atau frame.isConnected
. Jika properti ini
berubah untuk menunjukkan jendela yang ditutup atau iframe yang dihapus, sebaiknya jangan tetapkan
referensi apa pun ke jendela atau iframe 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
terjadi, yang disebut WeakRef
. WeakRef
yang dibuat untuk objek bukanlah referensi langsung, tetapi merupakan objek terpisah yang menyediakan metode .deref()
khusus yang menampilkan referensi ke objek selama belum dihapus oleh pembersihan sampah memori. Dengan WeakRef
, Anda dapat mengakses nilai jendela atau dokumen saat ini sekaligus tetap mengizinkan pengumpulan sampah. Daripada mempertahankan referensi ke jendela yang harus dibatalkan secara manual sebagai respons
terhadap peristiwa seperti pagehide
atau properti seperti window.closed
, akses ke jendela diperoleh
sesuai kebutuhan. Saat ditutup, jendela dapat di-garbage collection, sehingga 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 selama jangka waktu singkat setelah jendela ditutup atau
iframe dihapus. Hal ini karena WeakRef
terus menampilkan nilai hingga objek terkaitnya
telah di-garbage collection, yang terjadi secara asinkron di JavaScript dan umumnya selama waktu
tidak ada aktivitas. Untungnya, saat memeriksa jendela yang terpisah di panel Memory Chrome DevTools, mengambil
snapshot heap sebenarnya memicu pengumpulan sampah dan membuang jendela yang direferensikan secara lemah. Anda juga
dapat memeriksa apakah objek yang direferensikan melalui WeakRef
telah dihapus dari JavaScript,
baik dengan mendeteksi kapan deref()
menampilkan undefined
atau menggunakan
FinalizationRegistry
API 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 kapan jendela ditutup atau navigasi memuat ulang dokumen memberi kita cara untuk menghapus pengendali dan menghapus penetapan referensi sehingga jendela yang dilepas dapat dihapus sampahnya. Namun, perubahan ini adalah perbaikan khusus untuk masalah yang terkadang lebih mendasar: pengaitan langsung antara halaman.
Tersedia pendekatan alternatif yang lebih menyeluruh yang menghindari referensi yang sudah tidak berlaku antara jendela dan
dokumen: menetapkan pemisahan dengan membatasi komunikasi lintas dokumen ke
postMessage()
. Ingat 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, keduanya tidak mempertahankan referensi ke
dokumen saat ini dari jendela lain. Pendekatan penerusan pesan juga mendorong desain dengan
referensi jendela yang disimpan di satu tempat, yang berarti hanya satu referensi yang perlu dibatalkan penetapannya saat
jendela ditutup atau beralih. Pada contoh di atas, hanya showNotes()
yang mempertahankan referensi ke
jendela catatan, dan menggunakan peristiwa pagehide
untuk memastikan referensi tersebut dihapus.
Solusi: Hindari referensi yang menggunakan noopener
Jika jendela pop-up dibuka dan halaman Anda tidak perlu berkomunikasi atau mengontrolnya,
Anda mungkin dapat menghindari mendapatkan 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 membantu menemukan kebocoran di aplikasi Anda, beri tahu kami. Anda dapat menemukan saya di Twitter @_developit.