Evaluasi skrip dan tugas yang berjalan lama

Saat memuat skrip, browser membutuhkan waktu untuk mengevaluasinya sebelum dieksekusi, sehingga dapat mengakibatkan tugas yang berjalan lama. Pelajari cara kerja evaluasi skrip, dan tindakan yang dapat Anda lakukan agar tidak menimbulkan tugas yang berjalan lama selama pemuatan halaman.

Jika ingin mengoptimalkan Interaction to Next Paint (INP), sebagian besar saran yang akan Anda temui adalah mengoptimalkan interaksi itu sendiri. Misalnya, dalam panduan mengoptimalkan tugas panjang, teknik seperti menghasilkan dengan setTimeout, isInputPending, dan sebagainya dibahas. Teknik ini bermanfaat karena memberikan ruang kosong ke thread utama dengan menghindari tugas yang panjang, yang dapat memberikan lebih banyak peluang bagi interaksi dan aktivitas lainnya untuk berjalan lebih cepat, daripada jika mereka harus menunggu satu tugas yang panjang.

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 menjelaskan cara browser menangani tugas yang dimulai dengan evaluasi skrip, dan melihat apa yang dapat Anda lakukan untuk membagi tugas evaluasi skrip sehingga thread utama Anda dapat lebih responsif terhadap input pengguna saat halaman dimuat.

Apa itu evaluasi skrip?

Jika telah membuat profil aplikasi yang mengirimkan banyak JavaScript, Anda mungkin melihat tugas panjang yang pelakunya diberi label Evaluate Script.

Evaluasi skrip bekerja seperti yang divisualisasikan di profiler performa Chrome DevTools. Pekerjaan tersebut menyebabkan tugas yang panjang selama startup, yang memblokir kemampuan thread utama untuk merespons interaksi pengguna.
Evaluasi skrip bekerja seperti yang ditunjukkan di profiler performa di Chrome DevTools. Dalam kasus ini, tugas tersebut sudah cukup untuk menimbulkan tugas panjang yang memblokir thread utama agar tidak mengambil pekerjaan lain—termasuk tugas yang mendorong interaksi pengguna.

Evaluasi skrip merupakan bagian penting dari eksekusi JavaScript di browser, karena JavaScript dikompilasi tepat pada waktu sebelum dieksekusi. Saat dievaluasi, skrip akan diuraikan terlebih dahulu untuk menemukan error. Jika parser tidak menemukan error, skrip kemudian dikompilasi ke dalam bytecode, dan kemudian dapat melanjutkan eksekusi.

Meskipun perlu, evaluasi skrip dapat menimbulkan masalah, karena pengguna mungkin mencoba berinteraksi dengan halaman sesaat setelah halaman tersebut pertama kali dirender. Namun, hanya karena halaman telah dirender, bukan berarti halaman tersebut telah selesai dimuat. Interaksi yang terjadi selama pemuatan dapat tertunda karena halaman sedang sibuk mengevaluasi skrip. Meskipun tidak ada jaminan bahwa interaksi yang diinginkan dapat terjadi pada saat ini—karena skrip yang bertanggung jawab atas 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 atas evaluasi skrip dimulai bergantung pada apakah skrip yang Anda muat dimuat melalui elemen <script> reguler, atau apakah skrip adalah modul yang dimuat dengan type=module. Karena browser cenderung menangani sesuatu secara berbeda, cara mesin browser utama menangani evaluasi skrip akan dibahas jika perilaku evaluasi skrip di dalamnya bervariasi.

Memuat skrip 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 diurai, dikompilasi, dan dieksekusi. Hal ini berlaku untuk browser berbasis Chromium, Safari, dan Firefox.

Mengapa hal ini penting? Misalkan Anda menggunakan pemaket untuk mengelola skrip produksi, dan telah mengonfigurasinya untuk memaketkan semua yang dibutuhkan halaman agar menjalankan satu skrip. Jika hal ini terjadi pada situs web Anda, Anda dapat mengharapkan bahwa akan ada satu tugas yang dikirim untuk mengevaluasi skrip tersebut. Apakah ini hal yang buruk? Tidak harus—kecuali jika skrip tersebut besar.

Anda dapat memecah tugas evaluasi skrip dengan menghindari pemuatan bagian-bagian besar JavaScript, dan memuat skrip yang lebih kecil dan individual menggunakan elemen <script> tambahan.

Meskipun Anda harus selalu berusaha memuat JavaScript sesedikit mungkin selama pemuatan halaman, memisahkan skrip memastikan bahwa, sebagai ganti satu tugas besar yang dapat memblokir thread utama, Anda memiliki lebih banyak tugas kecil yang tidak akan memblokir thread utama sama sekali—atau setidaknya kurang dari apa yang Anda mulai.

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

Anda dapat menganggap memecah tugas untuk evaluasi skrip agak mirip dengan membuat callback peristiwa yang berjalan selama interaksi. Namun, dengan evaluasi skrip, mekanisme hasil membagi JavaScript yang Anda muat menjadi beberapa skrip yang lebih kecil, bukan jumlah skrip yang lebih besar daripada yang cenderung memblokir thread utama.

Memuat skrip dengan elemen <script> dan atribut type=module

Kini Anda dapat memuat modul ES secara native di browser dengan atribut type=module pada elemen <script>. Pendekatan untuk pemuatan skrip ini memberikan beberapa manfaat pengalaman developer, seperti tidak perlu mentransformasi kode untuk penggunaan produksi—terutama saat 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 darinya—memuat modul ES menggunakan atribut type=module akan menghasilkan jenis tugas yang berbeda dari yang biasanya Anda lihat saat tidak menggunakan type=module. Misalnya, tugas untuk setiap skrip modul akan dijalankan yang melibatkan aktivitas yang diberi label sebagai Modul Compile.

Kompilasi modul berfungsi dalam beberapa tugas seperti yang divisualisasikan di Chrome DevTools.
Perilaku pemuatan modul di browser berbasis Chromium. Setiap skrip modul akan memunculkan panggilan Compile module untuk mengompilasi konten sebelum evaluasi.

Setelah modul dikompilasi, kode apa pun yang selanjutnya dijalankan di dalamnya akan memulai aktivitas yang diberi label sebagai Evaluate module.

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

Dampaknya di sini—setidaknya di Chrome dan browser terkait—adalah langkah kompilasi akan terputus saat menggunakan modul ES. Hal ini jelas merupakan keuntungan dalam mengelola tugas yang berjalan lama; namun, hasil kerja evaluasi modul yang dihasilkan masih menunjukkan bahwa Anda menimbulkan beberapa biaya yang tidak terelakkan. 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 dibuat dalam konteks yang tidak ketat.
  • Skrip yang dimuat menggunakan type=module akan 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. Ini berarti Anda secara teori dapat memuat satu modul tingkat atas yang hanya terdiri dari pernyataan import statis ke modul lain, dan setiap modul yang dimuat akan menimbulkan tugas dan permintaan jaringan terpisah untuk mengevaluasinya.

Memuat skrip 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 keuntungan 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 membebaskan 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 tersebut dapat mengganggu kemampuan thread utama untuk merespons input pengguna jika interaksi terjadi 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.

Memuat skrip di pekerja web

Pekerja web adalah kasus penggunaan JavaScript khusus. Pekerja web didaftarkan di thread utama, dan kode dalam pekerja kemudian berjalan di threadnya sendiri. Hal ini sangat bermanfaat dalam arti bahwa—meskipun kode yang mendaftarkan pekerja web berjalan di thread utama—kode dalam pekerja web tidak. Hal ini akan mengurangi kemacetan thread utama, dan dapat membantu menjaga thread utama lebih responsif terhadap interaksi pengguna.

Selain mengurangi pekerjaan thread utama, pekerja web sendiri dapat memuat skrip eksternal untuk digunakan dalam konteks pekerja, baik melalui pernyataan importScripts maupun pernyataan import statis di browser yang mendukung pekerja modul. Hasilnya adalah setiap skrip yang diminta oleh pekerja web dievaluasi dari thread utama.

Kompromi dan pertimbangan

Meskipun memecah skrip menjadi beberapa file yang lebih kecil, Anda dapat membatasi tugas yang panjang dibandingkan dengan memuat file yang lebih sedikit dan jauh lebih besar. Oleh karena itu, penting untuk mempertimbangkan beberapa hal saat memutuskan cara membagi skrip.

Efisiensi kompresi

Kompresi adalah faktor untuk memecah skrip. Jika skrip lebih kecil, kompresi menjadi agak kurang efisien. Skrip yang lebih besar akan jauh lebih banyak menerima manfaat dari kompresi. Meskipun meningkatkan efisiensi kompresi membantu menjaga waktu pemuatan skrip serendah mungkin, ada sedikit langkah penyeimbangan untuk memastikan Anda memecah skrip menjadi potongan-potongan kecil yang cukup untuk memfasilitasi interaktivitas yang lebih baik selama startup.

Pemaket adalah alat yang ideal untuk mengelola ukuran output skrip yang menjadi dependensi situs Anda:

  • Jika terkait dengan webpack, plugin SplitChunksPlugin-nya dapat membantu. Lihat dokumentasi SplitChunksPlugin untuk mengetahui opsi yang dapat Anda tetapkan untuk membantu mengelola ukuran aset.
  • Untuk pemaket lainnya seperti Gabungan dan esbuild, Anda dapat mengelola ukuran file skrip menggunakan panggilan import() dinamis dalam kode Anda. Pemaket ini—serta webpack—akan otomatis memecah aset yang diimpor secara dinamis ke dalam filenya sendiri sehingga menghindari ukuran paket awal yang lebih besar.

Pembatalan validasi cache

Pembatalan cache berperan besar terhadap kecepatan pemuatan halaman pada kunjungan berulang. Saat Anda mengirimkan paket skrip monolitik yang besar, Anda akan dirugikan dalam hal penyimpanan cache browser. Hal ini terjadi karena saat Anda memperbarui kode pihak pertama—baik melalui update paket maupun perbaikan bug pengiriman—keseluruhan paket menjadi tidak valid dan harus didownload lagi.

Dengan memecah skrip, Anda tidak hanya memecah tugas evaluasi skrip menjadi tugas-tugas yang lebih kecil, tetapi juga meningkatkan kemungkinan pengunjung yang kembali mengambil lebih banyak skrip dari cache browser, bukan dari jaringan. Hal ini berarti pemuatan halaman yang lebih cepat secara keseluruhan.

Modul bertingkat dan performa pemuatan

Jika mengirimkan modul ES dalam produksi dan memuatnya dengan atribut type=module, Anda harus mengetahui bagaimana penyusunan bertingkat modul dapat memengaruhi waktu startup. Penyusunan bertingkat modul mengacu pada saat modul ES secara statis mengimpor modul ES lain yang secara statis mengimpor modul ES lain:

// a.js
import {b} from './b.js';

// b.js
import {c} from './c.js';

Jika modul ES Anda tidak digabungkan bersama, kode sebelumnya akan menghasilkan rantai permintaan jaringan: saat a.js diminta dari elemen <script>, permintaan jaringan lainnya akan dikirim untuk b.js, yang kemudian melibatkan permintaan lain untuk c.js. Salah satu cara untuk menghindari hal ini adalah dengan menggunakan pemaket—tetapi pastikan Anda mengonfigurasi pemaket untuk memisahkan skrip guna menyebarkan tugas evaluasi skrip.

Jika Anda tidak ingin menggunakan pemaket, cara lain untuk menghindari panggilan modul bertingkat adalah dengan menggunakan petunjuk resource modulepreload, yang akan melakukan pramuat modul ES terlebih dahulu untuk menghindari rantai permintaan jaringan.

Kesimpulan

Mengoptimalkan evaluasi skrip di browser tidak diragukan lagi adalah pekerjaan yang rumit. Pendekatannya bergantung pada persyaratan dan batasan situs Anda. Namun, dengan memisahkan skrip, Anda menyebarkan tugas evaluasi skrip ke banyak tugas yang lebih kecil, sehingga memberikan thread utama kemampuan untuk menangani interaksi pengguna dengan lebih efisien, daripada memblokir thread utama.

Sebagai rangkuman, 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 pemuatan skrip yang sangat besar, karena hal ini akan memulai tugas evaluasi skrip yang menggunakan banyak resource dan memblokir thread utama. Sebarkan skrip Anda ke elemen <script> lainnya untuk memisahkan tugas ini.
  • Menggunakan atribut type=module untuk memuat modul ES secara native di browser akan memulai setiap tugas untuk evaluasi bagi setiap skrip modul yang terpisah.
  • Kurangi ukuran paket awal Anda menggunakan panggilan import() dinamis. Ini juga berfungsi pada pemaket, karena pemaket akan memperlakukan setiap modul yang diimpor secara dinamis sebagai "titik terpisah", sehingga menghasilkan skrip terpisah untuk setiap modul yang diimpor secara dinamis.
  • Pastikan untuk mempertimbangkan kompromi seperti efisiensi kompresi dan pembatalan cache. Skrip yang lebih besar akan dikompresi dengan lebih baik, tetapi cenderung melibatkan pekerjaan evaluasi skrip yang lebih mahal dalam tugas yang lebih sedikit, dan mengakibatkan pembatalan cache browser, yang menyebabkan efisiensi caching yang lebih rendah secara keseluruhan.
  • Jika menggunakan modul ES secara native tanpa paket, gunakan petunjuk resource modulepreload untuk mengoptimalkan pemuatan modul tersebut selama startup.
  • Seperti biasa, kirimkan JavaScript sesedikit mungkin.

Anda memang perlu menyeimbangkan tugas, tetapi dengan memecah skrip dan mengurangi payload awal melalui 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 dalam metrik INP, sehingga memberikan pengalaman pengguna yang lebih baik.

Banner besar dari Unsplash, oleh Markus Spiske.