Memerangi kebocoran memori dalam aplikasi web

Ketika kami pindah dari pengembangan situs web, halaman yang dibentuk di server, ke pembuatan aplikasi web satu halaman yang diberikan pada klien, kami mengadopsi aturan tertentu dari permainan. Salah satunya adalah penanganan sumber daya yang akurat pada perangkat pengguna. Ini berarti - jangan menghalangi arus utama, jangan "memutar" kipas laptop, jangan meletakkan baterai ponsel. Kami bertukar peningkatan interaktivitas proyek web, dan fakta bahwa perilaku mereka menjadi lebih seperti perilaku aplikasi biasa, kelas masalah baru yang tidak ada di dunia rendering server.



Salah satu masalah tersebut adalah kebocoran memori. Aplikasi satu halaman yang dirancang dengan buruk dapat dengan mudah melahap megabita atau bahkan gigabita memori. Itu dapat mengambil lebih banyak dan lebih banyak sumber daya bahkan ketika itu duduk diam di tab latar belakang. Halaman aplikasi semacam itu, setelah menangkap sumber daya dalam jumlah yang sangat tinggi, mungkin mulai "melambat" dengan sangat. Selain itu, browser dapat dengan mudah mematikan tab dan memberi tahu pengguna: "Ada yang salah."


Ada yang salah

Tentu saja, situs yang dirender di server juga dapat mengalami masalah kebocoran memori. Tetapi di sini kita berbicara tentang memori server. Pada saat yang sama, sangat tidak mungkin aplikasi semacam itu akan menyebabkan kebocoran memori pada klien, karena browser menghapus memori setelah setiap transisi pengguna antar halaman.

Topik kebocoran memori tidak tercakup dengan baik dalam publikasi pengembangan web. Dan meskipun demikian, saya hampir yakin bahwa sebagian besar aplikasi halaman tunggal yang tidak sepele menderita kebocoran memori - kecuali tim yang berurusan dengan mereka memiliki alat yang dapat diandalkan untuk mendeteksi dan memperbaiki masalah ini. Intinya di sini adalah bahwa dalam JavaScript sangat mudah untuk secara acak mengalokasikan sejumlah memori, dan kemudian lupa untuk membebaskan memori ini.

Penulis artikel, terjemahan yang kami terbitkan hari ini, akan berbagi dengan pembaca pengalamannya dalam memerangi kebocoran memori dalam aplikasi web, dan juga ingin memberikan contoh deteksi efektif mereka.

Mengapa begitu sedikit yang ditulis tentang ini?


Pertama, saya ingin berbicara tentang mengapa sedikit sekali yang ditulis tentang kebocoran memori. Saya kira di sini Anda dapat menemukan beberapa alasan:

  • Kurangnya keluhan pengguna: sebagian besar pengguna tidak sibuk memantau task manager saat menjelajah web. Biasanya, pengembang tidak menemui keluhan pengguna hingga kebocoran memori sangat serius sehingga menyebabkan ketidakmampuan untuk bekerja atau memperlambat aplikasi.
  • : Chrome - , . .
  • : .
  • : «» . , , , , -.


Perpustakaan dan kerangka kerja modern untuk mengembangkan aplikasi web, seperti React, Vue dan Svelte, menggunakan model komponen aplikasi. Dalam model ini, cara paling umum untuk menyebabkan kebocoran memori adalah sesuatu seperti ini:

window.addEventListener('message', this.onMessage.bind(this));

Itu saja. Ini semua yang diperlukan untuk "melengkapi" proyek dengan kebocoran memori. Untuk melakukan ini, panggil saja metode addEventListener dari beberapa objek global (seperti window, atau <body>, atau sesuatu yang serupa), dan kemudian, ketika melepas komponen, lupa untuk menghapus pendengar acara menggunakan metode removeEventListener .

Tetapi konsekuensi dari ini bahkan lebih buruk, karena kebocoran seluruh komponen terjadi. Ini disebabkan oleh fakta bahwa metode tersebut this.onMessagedilampirkan this. Seiring dengan komponen ini, kebocoran komponen anak terjadi. Sangat mungkin bahwa semua node DOM yang terkait dengan komponen ini akan bocor. Akibatnya, situasi dapat keluar dari kendali dengan sangat cepat, yang mengakibatkan konsekuensi yang sangat buruk.

Inilah cara mengatasi masalah ini:

//   
this.onMessage = this.onMessage.bind(this);
window.addEventListener('message', this.onMessage);
 
//   
window.removeEventListener('message', this.onMessage);

Situasi di mana kebocoran memori paling sering terjadi


Pengalaman memberi tahu saya bahwa kebocoran memori paling sering terjadi ketika menggunakan API berikut:

  1. Metode addEventListener. Di sinilah kebocoran memori paling sering terjadi. Untuk mengatasi masalah, cukup menelepon pada waktu yang tepat removeEventListener.
  2. setTimeout setInterval. , (, 30 ), , , , , clearTimeout clearInterval. , setTimeout, «» , , setInterval-. , setTimeout .
  3. API IntersectionObserver, ResizeObserver, MutationObserver . , , . . - , , , , , disconnect . , DOM , , -. -, . — <body>, document, header footer, .
  4. Promise-, , . , , — , . , , «» , . «» .then()-.
  5. Repositori diwakili oleh objek global. Ketika Anda menggunakan sesuatu seperti Redux untuk mengontrol keadaan aplikasi , state store diwakili oleh objek global. Akibatnya, jika Anda berurusan dengan penyimpanan seperti itu dengan ceroboh, data yang tidak perlu tidak akan dihapus darinya, sehingga ukurannya akan terus meningkat.
  6. Pertumbuhan DOM yang tak terbatas. Jika halaman mengimplementasikan pengguliran tanpa akhir tanpa menggunakan virtualisasi , ini berarti bahwa jumlah node DOM pada halaman ini dapat tumbuh tanpa batas.

Di atas, kami memeriksa situasi di mana kebocoran memori paling sering terjadi, tetapi, tentu saja, ada banyak kasus lain yang menyebabkan masalah menarik bagi kami.

Identifikasi kebocoran memori


Sekarang kita telah beralih ke tantangan mengidentifikasi kebocoran memori. Untuk memulainya, saya tidak berpikir bahwa salah satu alat yang ada sangat cocok untuk ini. Saya mencoba alat analisis memori Firefox, mencoba alat dari Edge dan IE. Diuji bahkan Windows Performance Analyzer. Tetapi yang terbaik dari alat-alat ini masih Alat Pengembang Chrome. Benar, dalam alat ini ada banyak "sudut tajam", yang patut diketahui.

Di antara alat yang diberikan pengembang Chrome, kami paling tertarik pada profiler Heap snapshotdari tab Memory, yang memungkinkan Anda membuat foto tumpukan. Ada alat lain untuk menganalisis memori di Chrome, tetapi saya belum dapat mengekstrak manfaat khusus dari mereka dalam mendeteksi kebocoran memori.


Heap snapshot tool memungkinkan Anda mengambil snapshot dari memori aliran utama, pekerja web atau elemen iframe.Jika

jendela tool Chrome terlihat seperti yang ditunjukkan pada gambar sebelumnya, ketika Anda mengklik tombolTake snapshot, informasi tentang semua objek dalam memori mesin virtual yang dipilih ditangkap. JavaScript dari halaman yang diselidiki. Ini termasuk objek yang dirujukwindow, objek yang direferensikan oleh callback yang digunakan dalam panggilansetInterval, dan sebagainya. Sebuah snapshot dari memori dapat dianggap sebagai "momen beku" dari pekerjaan entitas yang diselidiki, mewakili informasi tentang semua memori yang digunakan oleh entitas ini.

Setelah foto diambil, kita sampai pada langkah berikutnya untuk menemukan kebocoran. Ini terdiri dalam mereproduksi skenario di mana, menurut pengembang, kebocoran memori dapat terjadi. Misalnya, membuka dan menutup jendela modal tertentu. Setelah jendela yang sama ditutup, diharapkan jumlah memori yang dialokasikan akan kembali ke tingkat yang ada sebelum jendela dibuka. Oleh karena itu, mereka mengambil gambar lain, dan kemudian membandingkannya dengan gambar yang diambil sebelumnya. Faktanya, perbandingan gambar adalah fitur yang paling penting yang menarik bagi kami Heap snapshot.


Kami mengambil snapshot pertama, lalu kami mengambil tindakan yang dapat menyebabkan kebocoran memori, dan kemudian kami mengambil snapshot lain. Jika tidak ada kebocoran, ukuran memori yang dialokasikan akan sama.

Benar, iniHeap snapshotjauh dari alat yang ideal. Ini memiliki beberapa batasan yang perlu diketahui:

  1. Bahkan jika Anda mengklik tombol kecil pada panel Memoryyang memulai pengumpulan sampah ( Collect garbage), maka untuk memastikan bahwa memori benar-benar dihapus, Anda mungkin perlu mengambil beberapa gambar berurutan. Saya biasanya memiliki tiga tembakan. Ini ada baiknya fokus pada ukuran total dari setiap gambar - itu, pada akhirnya, harus stabil.
  2. -, -, iframe, , , . , JavaScript. — , , .
  3. «». .

Pada titik ini, jika aplikasi Anda cukup kompleks, Anda mungkin memperhatikan banyak objek "bocor" ketika membandingkan foto. Di sini situasinya agak rumit, karena apa yang dapat disalahartikan sebagai kebocoran memori tidak selalu demikian. Banyak hal yang mencurigakan hanyalah proses normal untuk bekerja dengan objek. Memori yang ditempati oleh beberapa objek dihapus untuk menempatkan objek lain dalam memori ini, ada sesuatu yang memerah ke cache, dan sehingga memori yang sesuai tidak segera dihapus, dan sebagainya.

Kami melewati kebisingan informasi


Saya menemukan bahwa cara terbaik untuk menembus kebisingan informasi adalah mengulangi tindakan yang seharusnya menyebabkan kebocoran memori. Misalnya, alih-alih membuka dan menutup jendela modal hanya sekali setelah menangkap bidikan pertama, ini dapat dilakukan 7 kali. Mengapa 7? Ya, jika hanya karena 7 adalah prima yang terlihat. Maka Anda perlu mengambil bidikan kedua dan, membandingkannya dengan yang pertama, cari tahu apakah benda tertentu "bocor" 7 kali (atau 14 kali, atau 21 kali).


Bandingkan snapshot tumpukan. Harap perhatikan bahwa kami membandingkan gambar No. 3 dengan gambar No. 6. Faktanya adalah saya mengambil tiga pemotretan berturut-turut sehingga Chrome akan memiliki lebih banyak sesi pengumpulan sampah. Selain itu, perhatikan bahwa beberapa benda "bocor" sebanyak 7 kali.

Trik lain yang bermanfaat adalah bahwa, pada awal penelitian, sebelum membuat gambar pertama, lakukan prosedur satu kali, di mana, seperti yang diharapkan, kebocoran memori. Ini terutama disarankan jika pemisahan kode digunakan dalam proyek. Dalam kasus seperti itu, sangat mungkin bahwa pada pelaksanaan pertama dari tindakan yang mencurigakan, modul JavaScript yang diperlukan akan dimuat, yang akan mempengaruhi jumlah memori yang dialokasikan.

Sekarang Anda mungkin memiliki pertanyaan tentang mengapa Anda harus memberi perhatian khusus pada jumlah objek, dan bukan pada jumlah total memori. Di sini kita dapat mengatakan bahwa kita secara intuitif berusaha mengurangi jumlah "kebocoran" memori. Dalam hal ini, Anda mungkin berpikir bahwa Anda harus memantau jumlah total memori yang digunakan. Tetapi pendekatan ini, untuk satu alasan penting, tidak cocok untuk kita secara khusus.

Jika sesuatu “bocor”, itu terjadi karena (menceritakan kembali Joe Armstrong ) Anda memerlukan pisang, tetapi Anda berakhir dengan pisang, gorila yang memegangnya, dan juga, di samping itu, semua hutan. Jika kita fokus pada jumlah total ingatan, itu akan sama dengan "mengukur" hutan, dan bukan pisang yang menarik minat kita.


Gorila makan pisang.

Sekarang kembali ke contoh di atas denganaddEventListener. Sumber kebocoran adalah pendengar acara yang mereferensikan suatu fungsi. Dan fungsi ini, pada gilirannya, mengacu pada komponen yang, mungkin, menyimpan tautan ke banyak barang bagus seperti array, string, dan objek.

Jika Anda menganalisis perbedaan antara gambar, menyortir entitas dengan jumlah memori yang ditempati, ini akan memungkinkan Anda untuk melihat banyak array, garis, objek, yang sebagian besar kemungkinan besar tidak terkait dengan kebocoran. Dan setelah semua, kita perlu menemukan pendengar acara dari mana semuanya dimulai. Dia, dibandingkan dengan apa yang dia maksudkan, hanya menggunakan sedikit memori. Untuk memperbaiki kebocoran, Anda perlu menemukan pisang, bukan rimba.

Akibatnya, jika Anda mengurutkan catatan berdasarkan jumlah objek "bocor", Anda akan melihat 7 pendengar acara. Dan mungkin 7 komponen, dan 14 subkomponen, dan mungkin sesuatu yang lain seperti itu. Angka 7 ini harus menonjol dari gambaran besar, karena bagaimanapun, angka yang agak mencolok dan tidak biasa. Dalam hal ini, tidak masalah berapa kali tindakan mencurigakan diulang. Saat memeriksa gambar, jika kecurigaan dibenarkan, itu akan direkam seperti banyak objek "bocor". Ini adalah bagaimana Anda dapat dengan cepat mengidentifikasi sumber kebocoran memori.

Analisis Tautan Pohon


Alat untuk membuat snapshot menyediakan kemampuan untuk melihat "rantai penghubung" yang membantu Anda menemukan objek mana yang dirujuk oleh objek lain. Inilah yang memungkinkan aplikasi berfungsi. Dengan menganalisis "rantai" atau "pohon" tautan seperti itu, Anda dapat mengetahui dengan tepat di mana memori dialokasikan untuk objek "bocor".


Rantai tautan memungkinkan Anda menemukan objek mana yang merujuk ke objek "bocor". Saat membaca rantai ini, perlu diperhitungkan bahwa objek yang terletak di bawahnya merujuk ke objek yang terletak di atas.

Pada contoh di atas, ada variabel yang disebutsomeObjectdireferensikan dalam closure (context) yang dirujuk oleh event listener. Jika Anda mengklik tautan yang mengarah ke kode sumber, teks program yang cukup dapat dimengerti akan ditampilkan:

class SomeObject () { /* ... */ }
 
const someObject = new SomeObject();
const onMessage = () => { /* ... */ };
window.addEventListener('message', onMessage);

Jika kita membandingkan kode ini dengan gambar sebelumnya, ternyata contextangka itu adalah penutup onMessageyang merujuk someObject. Ini adalah contoh buatan . Kebocoran memori nyata bisa menjadi kurang jelas.

Perlu dicatat bahwa alat snapshot tumpukan memiliki beberapa keterbatasan:

  1. Jika Anda menyimpan file snapshot dan kemudian mengunggahnya lagi, tautan ke file dengan kode hilang. Misalnya, setelah mengunduh snapshot, tidak mungkin untuk mengetahui bahwa kode penutupan pendengar acara ada di baris 22 file foo.js. Karena informasi ini sangat penting, menyimpan file snapshot tumpukan, atau, misalnya, mentransfernya ke seseorang, hampir tidak berguna.
  2. WeakMap, Chrome , . , , , , . WeakMap — .
  3. Chrome , . , , , , . , object, EventListener. object — , , , «» 7 .

Ini adalah deskripsi strategi dasar saya untuk mengidentifikasi kebocoran memori. Saya telah berhasil menggunakan teknik ini untuk mendeteksi lusinan kebocoran.

Benar, saya harus mengatakan bahwa panduan ini untuk menemukan kebocoran memori hanya mencakup sebagian kecil dari apa yang terjadi dalam kenyataan. Ini baru permulaan pekerjaan. Selain itu, Anda harus dapat menangani pemasangan breakpoints, logging, pengujian koreksi untuk menentukan apakah mereka memecahkan masalah. Dan, sayangnya, semua ini, pada intinya, diterjemahkan menjadi investasi waktu yang serius.

Analisis kebocoran memori otomatis


Saya ingin memulai bagian ini dengan fakta bahwa saya tidak dapat menemukan pendekatan yang baik untuk mengotomatiskan deteksi kebocoran memori. Chrome memiliki performance.memory API sendiri , tetapi untuk alasan privasi, Chrome tidak memungkinkan Anda untuk mengumpulkan data yang cukup rinci. Akibatnya, API ini tidak dapat digunakan dalam produksi untuk mendeteksi kebocoran. Kelompok Kerja Kinerja Web W3C sebelumnya membahas alat memori, tetapi para anggotanya belum menyetujui standar baru yang dirancang untuk menggantikan API ini.

Di lingkungan pengujian, Anda dapat meningkatkan rincian output data performance.memorymenggunakan bendera Chrome - mengaktifkan-tepatnya-memori-info. Snapshot tumpukan masih dapat dibuat menggunakan tim Chromedriver sendiri : takeHeapSnapshot . Tim ini memiliki keterbatasan yang sama dengan yang telah kita diskusikan. Sangat mungkin bahwa jika Anda menggunakan perintah ini, maka, untuk alasan yang dijelaskan di atas, masuk akal untuk memanggilnya tiga kali, dan kemudian hanya menerima apa yang diterima sebagai hasil dari panggilan terakhirnya.

Karena pendengar peristiwa adalah sumber kebocoran memori yang paling umum, saya akan berbicara tentang teknik deteksi kebocoran lain yang saya gunakan. Ini terdiri dari membuat tambalan monyet untuk API addEventListenerdan removeEventListenerdalam menghitung tautan untuk memeriksa apakah jumlahnya kembali ke nol. Ini adalah contoh bagaimana hal ini dilakukan.

Di Alat Pengembang Chrome, Anda juga dapat menggunakan API asli getEventListeners untuk mengetahui pendengar acara mana yang dilampirkan ke elemen tertentu. Namun, perintah ini hanya tersedia dari bilah alat pengembang.

Saya ingin menambahkan bahwa Matthias Binens memberi tahu saya tentang API alat Chrome lain yang bermanfaat. Ini adalah queryObjects . Dengannya, Anda bisa mendapatkan informasi tentang semua objek yang dibuat menggunakan konstruktor tertentu. Berikut adalah beberapa materi bagus tentang topik ini tentang mengotomatisasi deteksi kebocoran memori di Puppeteer.

Ringkasan


Mencari dan memperbaiki kebocoran memori dalam aplikasi web masih dalam tahap awal. Di sini saya berbicara tentang beberapa teknik yang, dalam kasus saya, berkinerja baik. Tetapi harus diakui bahwa penerapan teknik ini masih penuh dengan kesulitan-kesulitan tertentu dan menyita waktu.

Seperti halnya masalah kinerja, seperti yang mereka katakan, sejumput sebelumnya bernilai satu pound. Mungkin seseorang akan merasa berguna untuk menyiapkan tes sintetik yang sesuai daripada menganalisis kebocoran setelah itu terjadi. Dan jika itu bukan satu kebocoran, tetapi beberapa, maka analisis masalah dapat berubah menjadi sesuatu seperti mengupas bawang: setelah satu masalah diperbaiki, yang lain ditemukan, dan kemudian proses ini berulang (dan selama ini, seperti dari bawang , air mata. Ulasan kode juga dapat membantu mengidentifikasi pola kebocoran umum. Tapi ini - jika Anda tahu - ke mana harus mencari.

JavaScript adalah bahasa yang memberikan keamanan bekerja dengan memori. Oleh karena itu, ada beberapa ironi dalam betapa mudahnya kebocoran memori terjadi dalam aplikasi web. Benar, ini sebagian karena fitur antarmuka pengguna perangkat. Anda perlu mendengarkan banyak acara: acara mouse, acara gulir, acara keyboard. Menerapkan semua pola ini dapat dengan mudah menyebabkan kebocoran memori. Tetapi, berusaha untuk memastikan bahwa aplikasi web kami menggunakan memori dengan hemat, kami dapat meningkatkan kinerjanya dan melindunginya dari "gangguan". Selain itu, dengan demikian kami menunjukkan penghormatan terhadap batas sumber daya perangkat pengguna.

Pembaca yang budiman! Pernahkah Anda mengalami kebocoran memori di proyek web Anda?


All Articles