Saat memuat skrip, browser memerlukan waktu untuk mengevaluasinya sebelum dieksekusi, yang dapat menyebabkan tugas yang lama. Pelajari cara kerja evaluasi skrip, dan tindakan yang dapat Anda lakukan untuk mencegahnya menyebabkan tugas yang lama selama pemuatan halaman.
Dalam hal mengoptimalkan Interaction to Next Paint (INP), sebagian besar saran yang akan Anda temukan adalah untuk mengoptimalkan interaksi itu sendiri. Misalnya, dalam panduan mengoptimalkan tugas panjang, teknik seperti menghasilkan dengan setTimeout
dan lainnya dibahas. Teknik ini bermanfaat, karena memungkinkan thread utama memiliki ruang untuk bernapas dengan menghindari tugas yang lama, yang dapat memberikan lebih banyak peluang bagi interaksi dan aktivitas lainnya untuk berjalan lebih cepat, daripada harus menunggu satu tugas yang lama.
Namun, bagaimana dengan tugas panjang yang berasal dari pemuatan skrip itu sendiri? Tugas ini dapat mengganggu interaksi pengguna dan memengaruhi INP halaman selama pemuatan. Panduan ini akan membahas cara browser menangani tugas yang dimulai oleh evaluasi skrip, dan melihat hal yang dapat Anda lakukan untuk membagi pekerjaan evaluasi skrip sehingga thread utama Anda dapat lebih responsif terhadap input pengguna saat halaman dimuat.
Apa yang dimaksud dengan evaluasi skrip?
Jika telah membuat profil aplikasi yang mengirimkan banyak JavaScript, Anda mungkin telah melihat tugas yang lama dengan pelaku yang diberi label Evaluate Script.
Evaluasi skrip adalah bagian yang diperlukan untuk menjalankan JavaScript di browser, karena JavaScript dikompilasi tepat waktu sebelum dieksekusi. Saat dievaluasi, skrip akan diuraikan terlebih dahulu untuk menemukan error. Jika parser tidak menemukan error, skrip kemudian dikompilasi menjadi bytecode, lalu dapat melanjutkan ke eksekusi.
Meskipun diperlukan, evaluasi skrip dapat bermasalah, karena pengguna mungkin mencoba berinteraksi dengan halaman segera setelah halaman tersebut dirender. Namun, hanya karena halaman telah dirender, bukan berarti halaman telah selesai dimuat. Interaksi yang terjadi selama pemuatan dapat tertunda karena halaman sedang sibuk mengevaluasi skrip. Meskipun tidak ada jaminan bahwa interaksi dapat terjadi pada saat ini—karena skrip yang bertanggung jawab untuknya mungkin belum dimuat—mungkin ada interaksi yang bergantung pada JavaScript yang sudah siap, atau interaktivitas tidak bergantung pada JavaScript sama sekali.
Hubungan antara skrip dan tugas yang mengevaluasinya
Cara tugas yang bertanggung jawab untuk evaluasi skrip dimulai bergantung pada apakah skrip yang Anda muat dimuat dengan elemen <script>
standar, atau apakah skrip tersebut adalah modul yang dimuat dengan type=module
. Karena browser cenderung menangani hal-hal secara berbeda, cara mesin browser utama menangani evaluasi skrip akan dibahas dengan perilaku evaluasi skrip yang berbeda-beda di antara mesin browser tersebut.
Skrip yang dimuat dengan elemen <script>
Jumlah tugas yang dikirim untuk mengevaluasi skrip umumnya memiliki hubungan langsung dengan jumlah elemen <script>
di halaman. Setiap elemen <script>
memulai tugas untuk mengevaluasi skrip yang diminta sehingga dapat diuraikan, dikompilasi, dan dieksekusi. Hal ini berlaku untuk browser berbasis Chromium, Safari, dan Firefox.
Mengapa hal ini penting? Misalnya, Anda menggunakan bundler untuk mengelola skrip produksi, dan telah mengonfigurasinya untuk menggabungkan semua yang diperlukan halaman Anda untuk dijalankan ke dalam satu skrip. Jika demikian untuk situs Anda, Anda dapat memperkirakan bahwa akan ada satu tugas yang dikirim untuk mengevaluasi skrip tersebut. Apakah ini hal yang buruk? Tidak selalu—kecuali jika skrip tersebut sangat besar.
Anda dapat membagi pekerjaan evaluasi skrip dengan menghindari pemuatan potongan besar JavaScript, dan memuat lebih banyak skrip individual yang lebih kecil menggunakan elemen <script>
tambahan.
Meskipun Anda harus selalu berusaha memuat JavaScript sesedikit mungkin selama pemuatan halaman, membagi skrip akan memastikan bahwa, alih-alih satu tugas besar yang dapat memblokir thread utama, Anda memiliki lebih banyak tugas kecil yang tidak akan memblokir thread utama sama sekali—atau setidaknya lebih sedikit dari yang Anda mulai.
Anda dapat menganggap pemisahan tugas untuk evaluasi skrip sebagai sesuatu yang mirip dengan memberikan hasil selama callback peristiwa yang berjalan selama interaksi. Namun, dengan evaluasi skrip, mekanisme yang menghasilkan akan memecah JavaScript yang Anda muat menjadi beberapa skrip yang lebih kecil, bukan sejumlah kecil skrip yang lebih besar yang cenderung memblokir thread utama.
Skrip yang dimuat dengan elemen <script>
dan atribut type=module
Sekarang Anda dapat memuat modul ES secara native di browser dengan atribut type=module
pada elemen <script>
. Pendekatan pemuatan skrip ini memiliki beberapa manfaat pengalaman developer, seperti tidak perlu mengubah kode untuk penggunaan produksi—terutama jika digunakan bersama dengan peta impor. Namun, memuat skrip dengan cara ini menjadwalkan tugas yang berbeda-beda di setiap browser.
Browser berbasis Chromium
Di browser seperti Chrome—atau yang berasal darinya—memuat modul ES menggunakan atribut type=module
menghasilkan berbagai jenis tugas yang berbeda dari yang biasanya Anda lihat saat tidak menggunakan type=module
. Misalnya, tugas untuk setiap skrip modul akan berjalan yang melibatkan aktivitas yang diberi label sebagai Compile module.
Setelah modul dikompilasi, kode apa pun yang kemudian berjalan di dalamnya akan memulai aktivitas yang diberi label Evaluasi modul.
Efeknya di sini—setidaknya di Chrome dan browser terkait—adalah bahwa langkah-langkah kompilasi dipecah saat menggunakan modul ES. Ini adalah keuntungan yang jelas dalam hal mengelola tugas yang panjang; namun, pekerjaan evaluasi modul yang dihasilkan masih berarti Anda akan mengalami beberapa biaya yang tidak dapat dihindari. Meskipun Anda harus berusaha mengirimkan JavaScript sesedikit mungkin, menggunakan modul ES—terlepas dari browser—memberikan manfaat berikut:
- Semua kode modul otomatis dijalankan dalam mode ketat, yang memungkinkan potensi pengoptimalan oleh mesin JavaScript yang tidak dapat dilakukan dalam konteks non-ketat.
- Skrip yang dimuat menggunakan
type=module
diperlakukan seolah-olah ditangguhkan secara default. Anda dapat menggunakan atributasync
pada skrip yang dimuat dengantype=module
untuk mengubah perilaku ini.
Safari dan Firefox
Saat modul dimuat di Safari dan Firefox, setiap modul dievaluasi dalam tugas terpisah. Artinya, secara teoritis Anda dapat memuat satu modul tingkat teratas yang hanya terdiri dari pernyataan import
statis ke modul lain, dan setiap modul yang dimuat akan menimbulkan permintaan dan tugas jaringan terpisah untuk mengevaluasinya.
Skrip yang dimuat dengan import()
dinamis
import()
dinamis adalah metode lain untuk memuat skrip. Tidak seperti pernyataan import
statis yang harus berada di bagian atas modul ES, panggilan import()
dinamis dapat muncul di mana saja dalam skrip untuk memuat potongan JavaScript sesuai permintaan. Teknik ini disebut pemisahan kode.
import()
dinamis memiliki dua keunggulan dalam hal meningkatkan INP:
- Modul yang ditangguhkan untuk dimuat nanti akan mengurangi pertentangan thread utama selama startup dengan mengurangi jumlah JavaScript yang dimuat pada saat itu. Tindakan ini akan mengosongkan thread utama sehingga dapat lebih responsif terhadap interaksi pengguna.
- Saat panggilan
import()
dinamis dilakukan, setiap panggilan akan secara efektif memisahkan kompilasi dan evaluasi setiap modul ke tugasnya sendiri. Tentu saja,import()
dinamis yang memuat modul yang sangat besar akan memulai tugas evaluasi skrip yang cukup besar, dan hal itu dapat mengganggu kemampuan thread utama untuk merespons input pengguna jika interaksi terjadi secara bersamaan dengan panggilanimport()
dinamis. Oleh karena itu, Anda harus memuat JavaScript sesedikit mungkin.
Panggilan import()
dinamis berperilaku serupa di semua mesin browser utama: tugas evaluasi skrip yang dihasilkan akan sama dengan jumlah modul yang diimpor secara dinamis.
Skrip yang dimuat di pekerja web
Web worker adalah kasus penggunaan JavaScript khusus. Web worker didaftarkan di thread utama, dan kode dalam pekerja kemudian berjalan di thread-nya sendiri. Hal ini sangat bermanfaat karena—meskipun kode yang mendaftarkan pekerja web berjalan di thread utama—kode dalam pekerja web tidak berjalan. Hal ini mengurangi kemacetan thread utama, dan dapat membantu menjaga thread utama agar lebih responsif terhadap interaksi pengguna.
Selain mengurangi pekerjaan thread utama, web worker sendiri dapat memuat skrip eksternal untuk digunakan dalam konteks pekerja, baik melalui pernyataan importScripts
atau import
statis di browser yang mendukung pekerja modul. Hasilnya adalah skrip apa pun yang diminta oleh web worker dievaluasi dari thread utama.
Kompromi dan pertimbangan
Meskipun membagi skrip menjadi file terpisah yang lebih kecil membantu membatasi tugas yang lama, bukan memuat lebih sedikit file yang jauh lebih besar, ada beberapa hal yang perlu dipertimbangkan saat memutuskan cara membagi skrip.
Efisiensi kompresi
Kompresi adalah faktor yang memengaruhi pemisahan skrip. Jika skrip lebih kecil, kompresi menjadi kurang efisien. Skrip yang lebih besar akan mendapatkan manfaat yang jauh lebih besar dari kompresi. Meskipun meningkatkan efisiensi kompresi membantu menjaga waktu pemuatan skrip serendah mungkin, Anda harus melakukan tindakan penyeimbangan untuk memastikan bahwa Anda membagi skrip menjadi beberapa bagian yang lebih kecil untuk memfasilitasi interaktivitas yang lebih baik selama startup.
Alat penggabungan adalah alat yang ideal untuk mengelola ukuran output untuk skrip yang menjadi dependensi situs Anda:
- Untuk webpack, plugin
SplitChunksPlugin
-nya dapat membantu. Lihat dokumentasiSplitChunksPlugin
untuk mengetahui opsi yang dapat Anda tetapkan untuk membantu mengelola ukuran aset. - Untuk bundler lain seperti Rollup dan esbuild, Anda dapat mengelola ukuran file skrip menggunakan panggilan
import()
dinamis dalam kode. Bundler ini—serta webpack—akan otomatis memisahkan aset yang diimpor secara dinamis ke dalam filenya sendiri, sehingga menghindari ukuran paket awal yang lebih besar.
Pembatalan validasi cache
Pembatalan validasi cache memainkan peran besar dalam kecepatan pemuatan halaman pada kunjungan berulang. Jika mengirimkan paket skrip monolitik yang besar, Anda akan dirugikan dalam hal penyimpanan dalam cache browser. Hal ini karena saat Anda mengupdate kode pihak pertama—baik melalui update paket maupun pengiriman perbaikan bug—seluruh paket akan menjadi tidak valid dan harus didownload lagi.
Dengan memecah skrip, Anda tidak hanya memecah pekerjaan evaluasi skrip di seluruh tugas yang lebih kecil, tetapi juga meningkatkan kemungkinan pengunjung yang kembali akan mengambil lebih banyak skrip dari cache browser, bukan dari jaringan. Hal ini berarti pemuatan halaman secara keseluruhan menjadi lebih cepat.
Modul bertingkat dan performa pemuatan
Jika Anda mengirimkan modul ES dalam produksi dan memuat modul tersebut dengan atribut type=module
, Anda harus mengetahui pengaruh tingkatan modul terhadap waktu startup. Penyusunan bertingkat modul mengacu pada saat modul ES mengimpor modul ES lain secara statis yang mengimpor modul ES lain secara statis:
// a.js
import {b} from './b.js';
// b.js
import {c} from './c.js';
Jika modul ES Anda tidak dipaketkan bersama, kode sebelumnya akan menghasilkan rantai permintaan jaringan: saat a.js
diminta dari elemen <script>
, permintaan jaringan lain akan dikirim untuk b.js
, yang kemudian melibatkan permintaan lain untuk c.js
. Salah satu cara untuk menghindari hal ini adalah dengan menggunakan bundler—tetapi pastikan Anda mengonfigurasi bundler untuk memecah skrip guna menyebarkan pekerjaan evaluasi skrip.
Jika Anda tidak ingin menggunakan bundler, cara lain untuk mengatasi panggilan modul bertingkat adalah menggunakan petunjuk resource modulepreload
, yang akan memuat modul ES terlebih dahulu untuk menghindari rantai permintaan jaringan.
Kesimpulan
Mengoptimalkan evaluasi skrip di browser tentu saja merupakan hal yang sulit. Pendekatannya bergantung pada persyaratan dan batasan situs Anda. Namun, dengan memisahkan skrip, Anda menyebarkan pekerjaan evaluasi skrip ke banyak tugas yang lebih kecil, sehingga memberi thread utama kemampuan untuk menangani interaksi pengguna secara lebih efisien, bukan memblokir thread utama.
Sebagai ringkasan, berikut beberapa hal yang dapat Anda lakukan untuk membagi tugas evaluasi skrip yang besar:
- Saat memuat skrip menggunakan elemen
<script>
tanpa atributtype=module
, hindari memuat skrip yang sangat besar, karena hal ini akan memulai tugas evaluasi skrip yang membutuhkan banyak resource yang memblokir thread utama. Sebarkan skrip Anda ke lebih banyak elemen<script>
untuk membagi pekerjaan ini. - Menggunakan atribut
type=module
untuk memuat modul ES secara native di browser akan memulai setiap tugas untuk evaluasi bagi setiap skrip modul terpisah. - Kurangi ukuran paket awal Anda dengan menggunakan panggilan
import()
dinamis. Hal ini juga berfungsi di bundler, karena bundler akan memperlakukan setiap modul yang diimpor secara dinamis sebagai "titik pemisahan", sehingga menghasilkan skrip terpisah yang dibuat untuk setiap modul yang diimpor secara dinamis. - Pastikan untuk mempertimbangkan kompromi seperti efisiensi kompresi dan pembatalan validasi cache. Skrip yang lebih besar akan dikompresi dengan lebih baik, tetapi lebih cenderung melibatkan pekerjaan evaluasi skrip yang lebih mahal dalam lebih sedikit tugas, dan mengakibatkan pembatalan validasi cache browser, sehingga efisiensi penyimpanan dalam cache secara keseluruhan menjadi lebih rendah.
- Jika menggunakan modul ES secara native tanpa bundling, gunakan petunjuk resource
modulepreload
untuk mengoptimalkan pemuatan modul tersebut selama startup. - Seperti biasa, kirim JavaScript sesedikit mungkin.
Ini tentu saja merupakan tindakan penyeimbangan—tetapi dengan memecah skrip dan mengurangi payload awal dengan import()
dinamis, Anda dapat mencapai performa startup yang lebih baik dan mengakomodasi interaksi pengguna dengan lebih baik selama periode startup yang penting tersebut. Hal ini akan membantu Anda mendapatkan skor yang lebih baik pada metrik INP, sehingga memberikan pengalaman pengguna yang lebih baik.