Evaluasi skrip dan tugas yang berjalan lama

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

Jika ingin mengoptimalkan Interaction to Next Paint (INP), saran yang akan Anda temui adalah dengan mengoptimalkan interaksi sendiri. Misalnya, dalam panduan mengoptimalkan tugas panjang, teknik seperti menghasilkan dengan setTimeout dan lainnya dibahas. Teknik ini bermanfaat karena memberikan ruang kosong pada thread utama dengan menghindari tugas yang berjalan lama, yang dapat memungkinkan lebih banyak peluang interaksi dan aktivitas lain berjalan lebih cepat, daripada jika 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 membahas bagaimana browser menangani tugas yang dimulai oleh evaluasi skrip, dan mengamati apa yang mungkin dapat Anda lakukan untuk memecah tugas evaluasi skrip sehingga thread utama 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 panjang yang penyebabnya diberi label Evaluate Script.

Evaluasi skrip berfungsi seperti yang divisualisasikan di profiler performa Chrome DevTools. Pekerjaan ini menyebabkan tugas yang panjang selama startup, yang memblokir kemampuan thread utama untuk merespons interaksi pengguna.
Pekerjaan evaluasi skrip seperti yang ditampilkan di profiler performa di Chrome DevTools. Dalam hal ini, pekerjaan tersebut cukup untuk menyebabkan tugas panjang yang menghalangi thread utama mengambil pekerjaan lain—termasuk tugas yang mendorong interaksi pengguna.

Evaluasi skrip adalah bagian penting dalam mengeksekusi 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 akan dikompilasi ke dalam bytecode, lalu dapat dilanjutkan ke eksekusi.

Walaupun perlu, evaluasi skrip bisa menjadi masalah, karena pengguna mungkin mencoba berinteraksi dengan laman tak lama setelah laman 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 sibuk mengevaluasi skrip. Meskipun tidak ada jaminan bahwa interaksi dapat terjadi pada waktu ini—karena skrip yang bertanggung jawab untuk interaksi tersebut 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 memulai tugas yang bertanggung jawab atas evaluasi skrip bergantung pada apakah skrip yang Anda muat dimuat dengan elemen <script> standar, atau apakah skrip adalah modul yang dimuat dengan type=module. Karena browser memiliki kecenderungan untuk menangani hal-hal secara berbeda, cara mesin browser utama menangani evaluasi skrip akan disinggung jika perilaku evaluasi skrip di semua browser tersebut bervariasi.

Skrip yang dimuat dengan elemen <script>

Jumlah tugas yang dikirimkan 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? Misalnya Anda menggunakan pemaket untuk mengelola skrip produksi, dan Anda telah mengonfigurasinya untuk memaketkan semua yang diperlukan halaman Anda agar berjalan ke dalam satu skrip. Jika hal ini terjadi di situs Anda, Anda dapat mengharapkan bahwa akan ada satu tugas yang dikirim untuk mengevaluasi skrip tersebut. Apakah ini hal yang buruk? Belum tentu—kecuali jika skrip tersebut besar.

Anda dapat memecah tugas 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 untuk memuat JavaScript sesedikit mungkin selama pemuatan halaman, membagi skrip Anda akan memastikan bahwa, alih-alih satu tugas besar yang dapat memblokir thread utama, Anda akan memiliki lebih banyak tugas lebih kecil yang tidak akan memblokir thread utama sama sekali—atau setidaknya kurang dari yang Anda mulai.

Beberapa tugas yang melibatkan evaluasi skrip seperti yang divisualisasikan di profiler performa Chrome DevTools. Karena beberapa skrip yang lebih kecil dimuat, bukan skrip yang lebih sedikit yang lebih besar, tugas cenderung tidak menjadi tugas yang panjang, sehingga thread utama dapat merespons input pengguna dengan lebih cepat.
Beberapa tugas dihasilkan untuk mengevaluasi skrip karena adanya beberapa elemen <script> di HTML halaman. Sebaiknya kirim satu paket skrip besar ke pengguna, yang kemungkinan besar akan memblokir thread utama.

Anda dapat menganggap pengelompokan tugas untuk evaluasi skrip mirip dengan menghasilkan selama callback peristiwa yang berjalan selama interaksi. Namun, dengan evaluasi skrip, mekanisme yang menghasilkan memecah JavaScript yang Anda muat menjadi beberapa skrip yang lebih kecil, bukan sejumlah kecil skrip yang lebih besar yang cenderung akan memblokir thread utama.

Skrip yang dimuat 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 pemuatan skrip ini membawa 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 pada 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 biasanya Anda lihat saat tidak menggunakan type=module. Misalnya, tugas untuk setiap skrip modul akan berjalan yang melibatkan aktivitas yang diberi label sebagai Modul Kompilasi.

Kompilasi modul berfungsi di beberapa tugas seperti yang divisualisasikan di Chrome DevTools.
Perilaku pemuatan modul di browser berbasis Chromium. Setiap skrip modul akan menghasilkan panggilan Modul Kompilasi untuk mengompilasi kontennya sebelum evaluasi.

Setelah modul dikompilasi, setiap kode yang selanjutnya berjalan di dalamnya akan memulai aktivitas yang diberi label sebagai Evaluate module.

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

Efeknya di sini—setidaknya di Chrome dan browser terkait—adalah langkah kompilasi tidak berfungsi saat menggunakan modul ES. Ini adalah kemenangan yang jelas dalam hal mengelola tugas yang berjalan lama. Namun, hasil evaluasi modul yang dihasilkan tetap berarti Anda mengeluarkan biaya yang tidak dapat dihindari. Meskipun Anda harus berusaha untuk mengirimkan JavaScript sesedikit mungkin, menggunakan modul ES—apa pun browsernya—memberikan manfaat berikut:

  • Semua kode modul otomatis dijalankan dalam mode ketat, yang memungkinkan pengoptimalan potensial oleh mesin JavaScript yang tidak dapat dilakukan dalam konteks yang tidak 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, masing-masing modul dievaluasi dalam tugas terpisah. Ini berarti secara teoretis Anda dapat memuat satu modul level teratas yang hanya terdiri dari pernyataan import statis ke modul lain, dan setiap modul yang dimuat akan mengeluarkan permintaan jaringan terpisah dan tugas untuk mengevaluasinya.

Skrip 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 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 dibuat, 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, penting untuk memuat sesedikit mungkin JavaScript.

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

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

Selain mengurangi pekerjaan thread utama, pekerja web itu sendiri dapat memuat skrip eksternal untuk digunakan dalam konteks pekerja, baik melalui pernyataan importScripts maupun 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 Anda menjadi file terpisah yang lebih kecil membantu membatasi tugas yang panjang dibandingkan dengan memuat lebih sedikit file yang jauh lebih besar, penting untuk mempertimbangkan beberapa hal saat memutuskan cara memecah skrip.

Efisiensi kompresi

Kompresi adalah salah satu faktor untuk memecah skrip. Jika skrip lebih kecil, kompresi menjadi agak kurang efisien. Skrip yang lebih besar akan mendapat manfaat lebih dari kompresi. Meskipun meningkatkan efisiensi kompresi membantu menjaga waktu pemuatan skrip tetap serendah mungkin, diperlukan penyeimbangan untuk memastikan bahwa Anda memecah skrip menjadi potongan-potongan kecil yang cukup untuk memfasilitasi interaktivitas yang lebih baik selama startup.

Bundler adalah alat yang ideal untuk mengelola ukuran output skrip yang diandalkan situs Anda:

  • Jika webpack terkait, plugin SplitChunksPlugin-nya dapat membantu. Baca dokumentasi SplitChunksPlugin untuk mengetahui opsi yang dapat Anda tetapkan guna membantu mengelola ukuran aset.
  • Untuk pemaket lainnya, seperti Rollup dan esbuild, Anda dapat mengelola ukuran file skrip menggunakan panggilan import() dinamis dalam kode Anda. Pemaket ini—serta webpack—akan secara 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 berperan penting dalam seberapa cepat halaman dimuat pada kunjungan berulang. Saat mengirim paket skrip monolitik yang besar, cache browser Anda kurang menguntungkan bagi Anda. Hal ini terjadi karena saat Anda memperbarui kode pihak pertama—baik melalui pembaruan paket maupun perbaikan bug pengiriman—seluruh paket menjadi tidak valid dan harus didownload lagi.

Dengan memecah skrip, Anda tidak hanya memecah tugas evaluasi skrip di tugas-tugas yang lebih kecil, Anda juga meningkatkan kemungkinan pengunjung yang kembali akan 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 Anda mengirimkan modul ES dalam produksi dan memuatnya dengan atribut type=module, Anda perlu mengetahui bagaimana penyusunan bertingkat modul dapat memengaruhi waktu startup. Penyusunan 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 dipaketkan, 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 pemaket, tetapi pastikan Anda mengonfigurasi pemaket Anda untuk memecah skrip guna menyebarkan tugas evaluasi skrip.

Jika Anda tidak ingin menggunakan bundler, 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. Pendekatan ini bergantung pada persyaratan dan kendala situs Anda. Namun, dengan memisahkan skrip, Anda menyebarkan pekerjaan evaluasi skrip pada banyak tugas yang lebih kecil, sehingga memberi thread utama kemampuan untuk menangani interaksi pengguna secara lebih efisien, daripada memblokir thread utama.

Sebagai rangkuman, berikut adalah beberapa hal yang dapat Anda lakukan untuk memecah tugas evaluasi skrip yang besar:

  • Saat memuat skrip menggunakan elemen <script> tanpa atribut type=module, hindari pemuatan skrip yang berukuran sangat besar, karena skrip tersebut akan memulai tugas evaluasi skrip yang menggunakan banyak resource yang memblokir thread utama. Sebarkan skrip Anda di lebih banyak elemen <script> untuk memecah tugas ini.
  • Menggunakan atribut type=module untuk memuat modul ES secara native di browser akan memulai setiap tugas untuk evaluasi setiap skrip modul terpisah.
  • Kurangi ukuran paket awal Anda dengan menggunakan panggilan import() dinamis. Cara ini juga berfungsi di pemaket, karena pemaket akan memperlakukan setiap modul yang diimpor secara dinamis sebagai "titik terpisah", sehingga menghasilkan skrip terpisah yang dihasilkan untuk setiap modul yang diimpor secara dinamis.
  • Pastikan untuk mempertimbangkan {i>trade-off<i} seperti efisiensi kompresi dan pembatalan {i>cache<i}. Skrip yang lebih besar akan dikompresi lebih baik, tetapi cenderung melibatkan pekerjaan evaluasi skrip yang lebih mahal dalam tugas yang lebih sedikit, dan mengakibatkan pembatalan cache browser, yang menyebabkan efisiensi penyimpanan cache yang lebih rendah secara keseluruhan.
  • Jika menggunakan modul ES secara native tanpa pemaketan, gunakan petunjuk resource modulepreload untuk mengoptimalkan pemuatannya selama startup.
  • Seperti biasa, kirimkan JavaScript sesedikit mungkin.

Hal ini merupakan tindakan penyeimbangan. Namun, dengan memecah skrip dan mengurangi payload awal menggunakan import() yang dinamis, Anda akan dapat mencapai performa startup yang lebih baik dan lebih mengakomodasi interaksi pengguna 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.

Banner besar dari Unsplash, oleh Markus Spiske.