Saran yang tersedia secara umum untuk membuat aplikasi JavaScript Anda lebih cepat sering kali mencakup "Jangan blokir thread utama" dan "Pecah tugas yang panjang". Halaman ini menguraikan arti dari saran tersebut, dan alasan pentingnya mengoptimalkan tugas di JavaScript.
Apa itu tugas?
Tugas adalah bagian pekerjaan terpisah yang dilakukan browser. Ini termasuk rendering, mengurai HTML dan CSS, menjalankan kode JavaScript yang Anda tulis, dan hal-hal lain yang mungkin tidak dapat Anda kontrol secara langsung. JavaScript halaman Anda adalah sumber utama tugas browser.
Tugas memengaruhi performa dalam beberapa cara. Misalnya, saat browser mendownload file JavaScript saat startup, browser akan mengantrekan tugas untuk diurai dan mengompilasi JavaScript tersebut agar dapat dieksekusi. Selanjutnya dalam siklus proses halaman, tugas lainnya dimulai saat JavaScript Anda berfungsi, seperti mendorong interaksi melalui pengendali peristiwa, animasi berbasis JavaScript, dan aktivitas latar belakang seperti pengumpulan analisis. Semua ini, kecuali pekerja web dan API serupa, terjadi di thread utama.
Apa yang dimaksud dengan thread utama?
Thread utama adalah tempat sebagian besar tugas berjalan di browser, dan tempat hampir semua JavaScript yang Anda tulis dijalankan.
Thread utama hanya dapat memproses satu tugas dalam satu waktu. Setiap tugas yang memerlukan waktu lebih dari 50 milidetik akan dihitung sebagai tugas panjang. Jika pengguna mencoba berinteraksi dengan halaman selama tugas yang panjang atau update rendering, browser harus menunggu untuk menangani interaksi tersebut, sehingga menyebabkan latensi.
Untuk mencegah hal ini, bagi setiap tugas yang panjang menjadi tugas-tugas kecil yang masing-masing membutuhkan lebih sedikit waktu untuk dijalankan. Hal ini disebut memisahkan tugas yang panjang.
Menghentikan tugas memberi browser lebih banyak peluang untuk merespons pekerjaan dengan prioritas lebih tinggi, termasuk interaksi pengguna, di antara tugas lainnya. Hal ini memungkinkan interaksi terjadi jauh lebih cepat, ketika pengguna mungkin mengalami jeda saat browser menunggu tugas yang lama selesai.
Strategi manajemen tugas
JavaScript memperlakukan setiap fungsi sebagai tugas tunggal karena menggunakan model eksekusi tugas run-to-completion. Artinya, fungsi yang memanggil beberapa fungsi lainnya, seperti contoh berikut, harus berjalan hingga semua fungsi yang dipanggil selesai, sehingga memperlambat browser:
function saveSettings () { //This is a long task.
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
Jika kode Anda berisi fungsi yang memanggil beberapa metode, bagi kode tersebut menjadi beberapa fungsi. Hal ini tidak hanya memberi browser lebih banyak peluang untuk merespons interaksi, tetapi juga membuat kode Anda lebih mudah dibaca, dikelola, dan ditulis untuk pengujian. Bagian berikut ini membahas beberapa strategi untuk memecah fungsi yang panjang dan memprioritaskan tugas yang menyusunnya.
Tunda eksekusi kode secara manual
Anda dapat menunda eksekusi beberapa tugas dengan meneruskan fungsi yang relevan ke
setTimeout()
. Ini berfungsi meskipun Anda menentukan waktu tunggu 0
.
function saveSettings () {
// Do critical work that is user-visible:
validateForm();
showSpinner();
updateUI();
// Defer work that isn't user-visible to a separate task:
setTimeout(() => {
saveToDatabase();
sendAnalytics();
}, 0);
}
Cara ini paling cocok untuk serangkaian fungsi yang perlu dijalankan secara berurutan. Kode
yang diatur secara berbeda memerlukan pendekatan yang berbeda. Contoh berikutnya adalah fungsi yang memproses data dalam jumlah besar menggunakan satu loop. Makin besar set data, makin lama waktu yang dibutuhkan, dan belum tentu ada tempat yang baik di dalam loop untuk menempatkan setTimeout()
:
function processData () {
for (const item of largeDataArray) {
// Process the individual item here.
}
}
Untungnya, ada beberapa API lain yang memungkinkan Anda mengalihkan eksekusi kode ke
tugas berikutnya. Sebaiknya gunakan postMessage()
untuk waktu tunggu yang lebih cepat.
Anda juga dapat memecah tugas menggunakan requestIdleCallback()
, tetapi tugas ini menjadwalkan tugas pada prioritas terendah dan hanya selama waktu tidak ada aktivitas browser. Artinya, jika thread utama sangat sibuk, tugas yang dijadwalkan dengan requestIdleCallback()
mungkin tidak akan pernah berjalan.
Gunakan async
/await
untuk membuat poin hasil
Untuk memastikan tugas penting yang ditampilkan kepada pengguna terjadi sebelum tugas yang berprioritas lebih rendah, buat thread utama dengan mengganggu task queue sebentar untuk memberi browser dapat menjalankan tugas yang lebih penting.
Cara paling jelas untuk melakukannya adalah dengan menggunakan Promise
yang di-resolve dengan panggilan ke
setTimeout()
:
function yieldToMain () {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
Dalam fungsi saveSettings()
, Anda dapat menyerah ke thread utama setelah setiap langkah jika Anda await
fungsi yieldToMain()
setelah setiap panggilan fungsi. Cara ini
secara efektif memecah tugas yang panjang menjadi beberapa tugas:
async function saveSettings () {
// Create an array of functions to run:
const tasks = [
validateForm,
showSpinner,
saveToDatabase,
updateUI,
sendAnalytics
]
// Loop over the tasks:
while (tasks.length > 0) {
// Shift the first task off the tasks array:
const task = tasks.shift();
// Run the task:
task();
// Yield to the main thread:
await yieldToMain();
}
}
Poin utama: Anda tidak harus menyerah setelah setiap panggilan fungsi. Misalnya, jika Anda menjalankan dua fungsi yang menghasilkan update penting pada antarmuka pengguna, Anda mungkin tidak ingin menyerah di antara fungsi tersebut. Jika Anda bisa, biarkan pekerjaan tersebut berjalan terlebih dahulu, kemudian pertimbangkan untuk menghasilkan antara fungsi yang melakukan pekerjaan latar belakang atau yang kurang penting yang tidak dilihat pengguna.
API penjadwal khusus
API yang disebutkan sejauh ini dapat membantu Anda membagi tugas, tetapi memiliki kelemahan yang signifikan: saat Anda menyerah ke thread utama dengan menunda kode untuk dijalankan di tugas berikutnya, kode tersebut akan ditambahkan ke akhir task queue.
Jika mengontrol semua kode di halaman, Anda dapat membuat penjadwal sendiri untuk memprioritaskan tugas. Namun, skrip pihak ketiga tidak akan menggunakan penjadwal, sehingga Anda tidak dapat benar-benar memprioritaskan pekerjaan dalam kasus tersebut. Anda hanya dapat memecahnya atau menghasilkan interaksi pengguna.
API penjadwal menawarkan fungsi postTask()
, yang memungkinkan
penjadwalan tugas yang lebih terperinci dan dapat membantu browser memprioritaskan pekerjaan sehingga
tugas berprioritas rendah akan diserahkan ke thread utama. postTask()
menggunakan promise dan menerima setelan priority
.
postTask()
API memiliki tiga prioritas yang tersedia:
'background'
untuk tugas dengan prioritas terendah.'user-visible'
untuk tugas prioritas sedang. Ini adalah default jika tidak adapriority
yang ditetapkan.'user-blocking'
untuk tugas penting yang perlu dijalankan dengan prioritas tinggi.
Kode contoh berikut menggunakan postTask()
API untuk menjalankan tiga tugas dengan
prioritas setinggi mungkin, dan dua tugas lainnya dengan prioritas
yang serendah mungkin:
function saveSettings () {
// Validate the form at high priority
scheduler.postTask(validateForm, {priority: 'user-blocking'});
// Show the spinner at high priority:
scheduler.postTask(showSpinner, {priority: 'user-blocking'});
// Update the database in the background:
scheduler.postTask(saveToDatabase, {priority: 'background'});
// Update the user interface at high priority:
scheduler.postTask(updateUI, {priority: 'user-blocking'});
// Send analytics data in the background:
scheduler.postTask(sendAnalytics, {priority: 'background'});
};
Di sini, prioritas tugas dijadwalkan agar tugas yang diprioritaskan untuk browser, seperti interaksi pengguna, dapat dilanjutkan.
Anda juga dapat membuat instance objek TaskController
berbeda yang berbagi
prioritas di antara tugas, termasuk kemampuan untuk mengubah prioritas untuk
berbagai instance TaskController
sesuai kebutuhan.
Hasil bawaan dengan kelanjutan menggunakan scheduler.yield()
API mendatang
Poin utama: Untuk penjelasan yang lebih mendetail tentang scheduler.yield()
, baca
uji coba originnya
(sejak selesai), serta penjelasannya.
Salah satu penambahan yang diusulkan untuk API penjadwal adalah scheduler.yield()
, API
yang dirancang khusus untuk menghasilkan thread utama di browser. Penggunaannya
mirip dengan fungsi yieldToMain()
yang ditunjukkan sebelumnya di halaman ini:
async function saveSettings () {
// Create an array of functions to run:
const tasks = [
validateForm,
showSpinner,
saveToDatabase,
updateUI,
sendAnalytics
]
// Loop over the tasks:
while (tasks.length > 0) {
// Shift the first task off the tasks array:
const task = tasks.shift();
// Run the task:
task();
// Yield to the main thread with the scheduler
// API's own yielding mechanism:
await scheduler.yield();
}
}
Kode ini sebagian besar sudah dikenal, tetapi alih-alih menggunakan yieldToMain()
, kode ini menggunakan
await scheduler.yield()
.
Manfaat scheduler.yield()
adalah kelanjutan, yang berarti jika Anda
menghasilkan di tengah serangkaian tugas, tugas terjadwal lainnya akan berlanjut dalam
urutan yang sama setelah titik hasil. Hal ini mencegah skrip pihak ketiga mengontrol urutan eksekusi kode Anda.
Penggunaan scheduler.postTask()
dengan priority: 'user-blocking'
juga memiliki
kemungkinan kelanjutan yang tinggi karena prioritas user-blocking
yang tinggi, sehingga Anda
dapat menggunakannya sebagai alternatif hingga scheduler.yield()
tersedia secara
lebih luas.
Penggunaan setTimeout()
(atau scheduler.postTask()
dengan priority: 'user-visible'
atau tanpa priority
eksplisit) menjadwalkan tugas di bagian belakang antrean, sehingga
tugas tertunda lainnya dapat berjalan sebelum kelanjutan.
Hasil atas input dengan isInputPending()
Dukungan Browser
- 87
- 87
- x
- x
isInputPending()
API menyediakan cara untuk memeriksa apakah pengguna mencoba berinteraksi dengan halaman
dan hanya menghasilkan jika input tertunda.
Hal ini memungkinkan JavaScript dilanjutkan jika tidak ada input yang tertunda, alih-alih menghasilkan dan berakhir di belakang task queue. Hal ini dapat menghasilkan peningkatan performa yang mengesankan, seperti yang dijelaskan dalam Intent to Ship, untuk situs yang mungkin tidak kembali ke thread utama.
Namun, sejak peluncuran API tersebut, pemahaman kami tentang hasil menjadi meningkat, terutama setelah diperkenalkannya INP. Kami tidak lagi merekomendasikan penggunaan API ini, dan sebagai gantinya merekomendasikan untuk menghasilkan, terlepas dari apakah input tertunda atau tidak. Perubahan pada rekomendasi ini karena sejumlah alasan:
- API mungkin salah menampilkan
false
dalam beberapa kasus saat pengguna telah berinteraksi. - Input bukan satu-satunya kasus saat tugas harus dihasilkan. Animasi dan update antarmuka pengguna reguler lainnya bisa sama pentingnya dalam menyediakan halaman web yang responsif.
- API hasil yang lebih komprehensif seperti
scheduler.postTask()
danscheduler.yield()
telah diperkenalkan untuk mengatasi masalah yang menyebabkan timbulnya masalah.
Kesimpulan
Mengelola tugas memang sulit, tetapi dengan melakukannya akan membantu halaman Anda merespons interaksi pengguna dengan lebih cepat. Ada berbagai teknik untuk mengelola dan memprioritaskan tugas tergantung pada kasus penggunaan Anda. Untuk mengulanginya, berikut adalah hal-hal utama yang perlu Anda pertimbangkan saat mengelola tugas:
- Menghasilkan ke thread utama untuk tugas penting yang dihadapi pengguna.
- Sebaiknya bereksperimen dengan
scheduler.yield()
. - Prioritaskan tugas dengan
postTask()
. - Terakhir, lakukan sesedikit mungkin pekerjaan dalam fungsi Anda.
Dengan satu atau beberapa alat ini, Anda seharusnya dapat menyusun pekerjaan dalam aplikasi sehingga dapat memprioritaskan kebutuhan pengguna sekaligus memastikan bahwa pekerjaan yang kurang penting masih dapat diselesaikan. Hal ini meningkatkan pengalaman pengguna dengan menjadikannya lebih responsif dan menyenangkan untuk digunakan.
Terima kasih banyak kepada Philip Walton atas pemeriksaan teknisnya terkait dokumen ini.
Gambar thumbnail bersumber dari Unsplash, dengan izin Amirali Mirhashemian.