Saat memuat skrip, browser memerlukan waktu untuk mengevaluasinya sebelum dieksekusi, yang dapat menyebabkan tugas yang panjang. Pelajari cara kerja evaluasi skrip, dan hal yang dapat Anda lakukan untuk mencegahnya menyebabkan tugas yang panjang selama pemuatan halaman.
Saat mengoptimalkan Interaction to Next Paint (INP), sebagian besar saran yang akan Anda temui adalah mengoptimalkan interaksi itu sendiri. Misalnya, dalam panduan mengoptimalkan tugas yang panjang, teknik seperti hasil dengan setTimeout dan lainnya dibahas. Teknik ini bermanfaat, karena memungkinkan thread utama memiliki waktu untuk bernapas dengan menghindari tugas berdurasi lama, yang dapat memberikan lebih banyak peluang untuk interaksi dan aktivitas lainnya berjalan lebih cepat, daripada jika harus menunggu satu tugas berdurasi lama.
Namun, bagaimana dengan tugas yang 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 apa yang dapat Anda lakukan untuk memecah pekerjaan evaluasi skrip sehingga thread utama Anda dapat lebih responsif terhadap input pengguna saat halaman dimuat.
Apa yang dimaksud dengan evaluasi skrip?
Jika Anda telah membuat profil aplikasi yang mengirimkan banyak JavaScript, Anda mungkin telah melihat tugas yang panjang dengan penyebab yang diberi label Evaluasi Skrip.
Evaluasi skrip adalah bagian yang diperlukan untuk menjalankan JavaScript di browser, karena JavaScript dikompilasi tepat waktu sebelum dieksekusi. Saat skrip dievaluasi, skrip tersebut pertama-tama diuraikan untuk mencari error. Jika parser tidak menemukan error, skrip akan dikompilasi menjadi bytecode, lalu dapat melanjutkan ke eksekusi.
Meskipun diperlukan, evaluasi skrip dapat menjadi masalah, karena pengguna mungkin mencoba berinteraksi dengan halaman segera setelah halaman dirender untuk pertama kalinya. 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 untuk interaksi tersebut mungkin belum dimuat—mungkin ada interaksi yang bergantung pada JavaScript yang 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> biasa, atau jika skrip adalah modul yang dimuat dengan type=module. Karena browser cenderung menangani hal-hal secara berbeda, cara mesin browser utama menangani evaluasi skrip akan dibahas jika perilaku evaluasi skrip di seluruh browser tersebut bervariasi.
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 Anda telah mengonfigurasinya untuk memaketkan semua yang dibutuhkan halaman Anda untuk dijalankan ke dalam satu skrip. Jika hal ini berlaku untuk situs Anda, Anda dapat memperkirakan bahwa akan ada satu tugas yang dikirim untuk mengevaluasi skrip tersebut. Apakah itu hal yang buruk? Tidak selalu—kecuali jika skrip tersebut sangat besar.
Anda dapat memecah pekerjaan evaluasi skrip dengan menghindari pemuatan JavaScript dalam jumlah besar, dan memuat lebih banyak skrip individual yang lebih kecil menggunakan elemen <script> tambahan.
Meskipun Anda harus selalu berupaya memuat JavaScript sesedikit mungkin selama pemuatan halaman, membagi skrip Anda akan memastikan bahwa, alih-alih satu tugas besar yang dapat memblokir thread utama, Anda memiliki lebih banyak tugas yang lebih kecil yang tidak akan memblokir thread utama sama sekali—atau setidaknya lebih sedikit dari yang Anda mulai.
<script> yang ada di HTML halaman. Hal ini lebih baik daripada mengirim satu paket skrip besar kepada pengguna, yang lebih mungkin memblokir thread utama.
Anda dapat menganggap pemecahan tugas untuk evaluasi skrip agak mirip dengan hasil selama callback peristiwa yang berjalan selama interaksi. Namun, dengan evaluasi skrip, mekanisme hasil memecah JavaScript yang Anda muat menjadi beberapa skrip yang lebih kecil, bukan sejumlah kecil skrip yang lebih besar yang lebih mungkin memblokir thread utama.
Skrip yang dimuat dengan elemen <script> dan atribut type=module
Sekarang Anda dapat memuat modul ES secara native di browser dengan type=module atribut pada <script> elemen. Pendekatan pemuatan skrip ini memberikan 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 akan menjadwalkan tugas yang berbeda dari browser ke browser.
Browser berbasis Chromium
Di browser seperti Chrome—atau yang berasal dari Chrome—memuat modul ES menggunakan atribut type=module menghasilkan 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 Kompilasi modul.
Setelah modul dikompilasi, kode apa pun yang kemudian berjalan di dalamnya akan memulai aktivitas yang diberi label Evaluasi modul.
Efek di sini—setidaknya di Chrome dan browser terkait—adalah langkah-langkah kompilasi dipecah saat menggunakan modul ES. Ini adalah kemenangan yang jelas dalam hal pengelolaan tugas yang panjang; namun, pekerjaan evaluasi modul yang dihasilkan masih berarti Anda mengeluarkan biaya yang tidak dapat dihindari. Meskipun Anda harus berupaya mengirimkan JavaScript sesedikit mungkin, menggunakan modul ES—terlepas dari browser—memberikan manfaat berikut:
- Semua kode modul otomatis dijalankan dalam mode ketat, yang memungkinkan pengoptimalan potensial oleh mesin JavaScript yang tidak dapat dilakukan dalam konteks non-ketat.
- Skrip yang dimuat menggunakan
type=modulediperlakukan seolah-olah ditangguhkan secara default. Anda dapat menggunakan atributasyncpada skrip yang dimuat dengantype=moduleuntuk mengubah perilaku ini.
Safari dan Firefox
Saat modul dimuat di Safari dan Firefox, setiap modul dievaluasi dalam tugas terpisah. Artinya, secara teori Anda dapat memuat satu modul tingkat atas yang hanya terdiri dari pernyataan statis import ke modul lain, dan setiap modul yang dimuat akan dikenai permintaan jaringan dan tugas terpisah untuk mengevaluasinya.
Skrip yang dimuat dengan import() dinamis
Dinamis import() 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 bagian 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. Hal ini membebaskan 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 ini dapat mengganggu kemampuan thread utama untuk merespons input pengguna jika interaksi terjadi pada saat yang sama dengan panggilanimport()dinamis. Oleh karena itu, Anda tetap 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 web worker
Web worker adalah kasus penggunaan JavaScript khusus. Web worker terdaftar di thread utama, dan kode dalam worker kemudian berjalan di thread-nya sendiri. Hal ini sangat bermanfaat karena—meskipun kode yang mendaftarkan web worker berjalan di thread utama—kode dalam web worker tidak. Hal ini mengurangi kemacetan thread utama, dan dapat membantu menjaga thread utama lebih responsif terhadap interaksi pengguna.
Selain mengurangi pekerjaan thread utama, web worker itu sendiri dapat memuat skrip eksternal untuk digunakan dalam konteks worker, baik melalui importScripts atau pernyataan import statis di browser yang mendukung worker modul. Hasilnya adalah skrip apa pun yang diminta oleh web worker dievaluasi di luar thread utama.
Kompromi dan pertimbangan
Meskipun memecah skrip Anda menjadi file yang terpisah dan lebih kecil membantu membatasi tugas yang panjang dibandingkan dengan memuat lebih sedikit file yang jauh lebih besar, Anda harus mempertimbangkan beberapa hal saat memutuskan cara memecah skrip.
Efisiensi kompresi
Kompresi adalah faktor penting saat memecah 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, hal ini merupakan tindakan penyeimbangan untuk memastikan Anda memecah skrip menjadi cukup banyak bagian yang lebih kecil untuk memfasilitasi interaktivitas yang lebih baik selama startup.
Bundler adalah alat yang ideal untuk mengelola ukuran output skrip yang bergantung pada situs Anda:
- Untuk webpack, plugin
SplitChunksPlugindapat membantu. Lihat dokumentasiSplitChunksPluginuntuk mengetahui opsi yang dapat Anda tetapkan untuk membantu mengelola ukuran aset. - Untuk bundler lain seperti Rollup dan esbuild, Anda dapat mengelola ukuran file skrip dengan menggunakan panggilan
import()dinamis dalam kode Anda. Bundler ini—serta webpack—akan otomatis memisahkan aset yang diimpor secara dinamis ke dalam file-nya sendiri, sehingga menghindari ukuran paket awal yang lebih besar.
Pembatalan validasi cache
Pembatalan validasi cache memainkan peran besar dalam seberapa cepat halaman dimuat pada kunjungan berulang. Saat Anda mengirimkan paket skrip monolitik yang besar, Anda akan dirugikan dalam hal caching browser. Hal ini karena saat Anda mengupdate kode pihak pertama—baik dengan mengupdate paket atau mengirimkan perbaikan bug—seluruh paket akan dibatalkan validasinya dan harus didownload lagi.
Dengan memecah skrip, Anda tidak hanya memecah pekerjaan evaluasi skrip ke dalam tugas yang lebih kecil, tetapi juga meningkatkan kemungkinan pengunjung yang kembali akan mengambil lebih banyak skrip dari cache browser, bukan dari jaringan. Hal ini akan menghasilkan pemuatan halaman yang lebih cepat secara keseluruhan.
Modul bertingkat dan performa pemuatan
Jika Anda mengirimkan modul ES dalam produksi dan memuatnya dengan atribut type=module, Anda harus mengetahui bagaimana nesting modul dapat memengaruhi waktu startup. Nesting modul mengacu pada saat modul ES mengimpor secara statis modul ES lain yang mengimpor secara statis modul ES lain:
// 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 menghindarinya 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 tidak diragukan lagi merupakan tugas yang sulit. Pendekatan ini bergantung pada persyaratan dan batasan situs Anda. Namun, dengan memecah skrip, Anda menyebarkan pekerjaan evaluasi skrip ke banyak tugas yang lebih kecil, sehingga memberikan kemampuan kepada thread utama untuk menangani interaksi pengguna dengan lebih efisien, bukan memblokir thread utama.
Sebagai ringkasan, berikut beberapa hal yang dapat Anda lakukan untuk memecah tugas evaluasi skrip yang besar:
- Saat memuat skrip menggunakan elemen
<script>tanpa atributtype=module, hindari memuat skrip yang sangat besar, karena skrip ini akan memulai tugas evaluasi skrip yang menggunakan banyak resource yang memblokir thread utama. Sebarkan skrip Anda ke lebih banyak elemen<script>untuk memecah pekerjaan ini. - Menggunakan atribut
type=moduleuntuk memuat modul ES secara native di browser akan memulai tugas individual untuk evaluasi 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 untuk setiap modul yang diimpor secara dinamis. - Pastikan untuk menimbang kompromi seperti efisiensi kompresi dan pembatalan validasi cache. Skrip yang lebih besar akan dikompresi dengan lebih baik, tetapi lebih mungkin melibatkan pekerjaan evaluasi skrip yang lebih mahal dalam lebih sedikit tugas, dan mengakibatkan pembatalan validasi cache browser, sehingga menyebabkan efisiensi caching yang lebih rendah secara keseluruhan.
- Jika menggunakan modul ES secara native tanpa pemaketan, gunakan petunjuk resource
modulepreloaduntuk mengoptimalkan pemuatannya selama startup. - Seperti biasa, kirimkan JavaScript sesedikit mungkin.
Hal ini tentu 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.