Evaluasi skrip dan tugas yang berjalan lama

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 berfungsi seperti yang divisualisasi dalam profiler performa Chrome DevTools. Pekerjaan tersebut menyebabkan tugas yang lama selama startup, yang memblokir kemampuan thread utama untuk merespons interaksi pengguna.
Fungsi evaluasi skrip seperti yang ditampilkan di profiler performa di Chrome DevTools. Dalam hal ini, pekerjaan tersebut cukup untuk menyebabkan tugas yang lama yang memblokir thread utama agar tidak melakukan pekerjaan lain—termasuk tugas yang mendorong interaksi pengguna.

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 berbagai hal secara berbeda, cara mesin browser utama menangani evaluasi skrip akan dibahas dengan perilaku evaluasi skrip yang berbeda 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.

Beberapa tugas yang melibatkan evaluasi skrip seperti yang divisualisasi dalam profiler performa Chrome DevTools. Karena beberapa skrip yang lebih kecil dimuat, bukan skrip yang lebih besar, tugas cenderung tidak menjadi tugas yang panjang, sehingga thread utama dapat merespons input pengguna dengan lebih cepat.
Beberapa tugas dibuat untuk mengevaluasi skrip sebagai hasil dari beberapa elemen <script> yang ada di HTML halaman. Hal ini lebih baik daripada mengirim satu paket skrip besar kepada pengguna, yang lebih cenderung memblokir thread utama.

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 akan 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.

Kompilasi modul bekerja dalam beberapa tugas seperti yang divisualisasi di Chrome DevTools.
Pola pemuatan modul di browser berbasis Chromium. Setiap skrip modul akan membuat panggilan Compile module untuk mengompilasi kontennya sebelum evaluasi.

Setelah modul dikompilasi, kode apa pun yang kemudian berjalan di dalamnya akan memulai aktivitas yang diberi label Evaluasi modul.

Evaluasi tepat waktu dari modul seperti yang divisualisasi di panel performa Chrome DevTools.
Saat kode dalam modul berjalan, modul tersebut akan dievaluasi tepat waktu.

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 atribut async pada skrip yang dimuat dengan type=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:

  1. 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.
  2. 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 panggilan import() 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 dokumentasi SplitChunksPlugin 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 akan menghasilkan pemuatan halaman yang lebih cepat secara keseluruhan.

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 atribut type=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.