Anda diberi tahu "jangan blokir thread utama" dan "hentikan tugas yang lama", tetapi apa artinya melakukan hal-hal itu?
Saran umum untuk menjaga aplikasi JavaScript tetap cepat cenderung bermuara pada saran berikut:
- "Jangan memblokir thread utama".
- "Bagi tugas panjang Anda menjadi beberapa bagian."
Ini adalah saran yang bagus, tetapi apa fungsinya? Pengiriman lebih sedikit JavaScript bagus, tetapi apakah hal ini akan menjadikan antarmuka pengguna lebih responsif secara otomatis? Mungkin ya, mungkin tidak.
Untuk memahami cara mengoptimalkan tugas di JavaScript, Anda harus terlebih dahulu mengetahui tugas apa saja, dan bagaimana browser menanganinya.
Apa yang dimaksud dengan tugas?
Tugas adalah bagian pekerjaan terpisah yang dilakukan browser. Pekerjaan tersebut mencakup rendering, mengurai HTML dan CSS, menjalankan JavaScript, dan jenis pekerjaan lainnya yang mungkin tidak dapat Anda kontrol secara langsung. Dari semua ini, JavaScript yang Anda tulis mungkin merupakan sumber tugas terbesar.
Tugas yang terkait dengan JavaScript memengaruhi performa dengan beberapa cara:
- Ketika browser mengunduh file JavaScript saat startup, browser akan mengantrekan tugas untuk mengurai dan mengompilasi JavaScript tersebut sehingga dapat dieksekusi nanti.
- Pada waktu lain selama masa aktif halaman, tugas akan diantrekan saat JavaScript berfungsi, seperti mendorong interaksi melalui pengendali peristiwa, animasi yang didorong JavaScript, dan aktivitas latar belakang seperti pengumpulan analisis.
Semua hal ini—kecuali web worker dan API serupa—terjadi di thread utama.
Apa thread utamanya?
Thread utama adalah tempat sebagian besar tugas berjalan di browser, dan tempat hampir semua JavaScript yang Anda tulis dieksekusi.
Thread utama hanya dapat memproses satu tugas dalam satu waktu. Setiap tugas yang memerlukan waktu lebih dari 50 milidetik adalah tugas yang lama. Untuk tugas yang melebihi 50 milidetik, total waktu tugas dikurangi 50 milidetik dikenal sebagai periode pemblokiran tugas.
Browser memblokir interaksi agar tidak terjadi saat tugas berdurasi berapa pun sedang berjalan, tetapi hal ini tidak dapat dilihat oleh pengguna selama tugas tidak berjalan terlalu lama. Namun, saat pengguna mencoba berinteraksi dengan halaman saat ada banyak tugas yang lama, antarmuka pengguna akan terasa tidak responsif, dan mungkin bahkan rusak jika thread utama diblokir untuk jangka waktu yang sangat lama.
Untuk mencegah thread utama diblokir terlalu lama, Anda dapat membagi tugas yang panjang menjadi beberapa tugas yang lebih kecil.
Hal ini penting karena ketika tugas dipisahkan, browser bisa merespons pekerjaan dengan prioritas lebih tinggi jauh lebih cepat—termasuk interaksi pengguna. Setelah itu, tugas yang tersisa kemudian dijalankan hingga selesai, memastikan pekerjaan yang awalnya Anda antrikan selesai.
Di bagian atas gambar sebelumnya, pengendali peristiwa yang diantrekan oleh interaksi pengguna harus menunggu satu tugas panjang sebelum dapat dimulai. Hal ini menunda interaksi yang terjadi. Dalam skenario ini, pengguna mungkin telah melihat jeda. Di bagian bawah, pengendali peristiwa dapat mulai berjalan lebih cepat, dan interaksinya mungkin terasa instan.
Setelah mengetahui pentingnya membagi tugas, Anda dapat mempelajari cara melakukannya di JavaScript.
Strategi pengelolaan tugas
Saran umum dalam arsitektur perangkat lunak adalah membagi pekerjaan Anda menjadi fungsi-fungsi yang lebih kecil:
function saveSettings () {
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
Dalam contoh ini, ada fungsi bernama saveSettings()
yang memanggil lima fungsi untuk memvalidasi formulir, menampilkan indikator lingkaran berputar, mengirim data ke backend aplikasi, memperbarui antarmuka pengguna, dan mengirim analisis.
Secara konseptual, saveSettings()
dirancang dengan baik. Jika perlu men-debug salah satu fungsi ini, Anda dapat menjelajahi hierarki project untuk mengetahui fungsi setiap fungsi. Dengan membagi pekerjaan seperti ini, project akan lebih mudah dikelola dan dipelihara.
Namun, potensi masalah di sini adalah JavaScript tidak menjalankan setiap fungsi ini sebagai tugas terpisah karena fungsi tersebut dieksekusi dalam fungsi saveSettings()
. Artinya, kelima fungsi tersebut akan berjalan sebagai satu tugas.
Dalam skenario kasus terbaik, bahkan hanya satu dari fungsi tersebut yang dapat berkontribusi 50 milidetik atau lebih terhadap total panjang tugas. Dalam kasus terburuk, lebih banyak tugas tersebut dapat berjalan lebih lama—terutama di perangkat yang sumber dayanya terbatas.
Menunda eksekusi kode secara manual
Salah satu metode yang digunakan developer untuk membagi tugas menjadi tugas yang lebih kecil melibatkan setTimeout()
. Dengan teknik ini, Anda meneruskan fungsi ke setTimeout()
. Tindakan ini akan menunda eksekusi callback ke tugas terpisah, 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);
}
Hal ini dikenal sebagai menghasilkan, dan berfungsi paling baik untuk serangkaian fungsi yang perlu dijalankan secara berurutan.
Namun, kode Anda mungkin tidak selalu diatur dengan cara ini. Misalnya, Anda dapat memiliki data dalam jumlah besar yang perlu diproses dalam loop, dan tugas tersebut dapat memerlukan waktu yang sangat lama jika ada banyak iterasi.
function processData () {
for (const item of largeDataArray) {
// Process the individual item here.
}
}
Penggunaan setTimeout()
di sini bermasalah karena ergonomi developer, dan seluruh array data dapat memerlukan waktu yang sangat lama untuk diproses, meskipun setiap iterasi berjalan dengan cepat. Semuanya akan bertambah, dan setTimeout()
bukanlah alat yang tepat untuk tugas ini—setidaknya tidak jika digunakan dengan cara ini.
Gunakan async
/await
untuk membuat poin hasil
Untuk memastikan tugas penting yang ditampilkan kepada pengguna terjadi sebelum tugas berprioritas lebih rendah, Anda dapat menghasilkan thread utama dengan mengganggu task queue sebentar untuk memberi browser kesempatan menjalankan tugas yang lebih penting.
Seperti yang dijelaskan sebelumnya, setTimeout
dapat digunakan untuk menghasilkan thread utama. Namun, agar memberikan kenyamanan dan keterbacaan yang lebih baik, Anda dapat memanggil setTimeout
dalam Promise
dan meneruskan metode resolve
sebagai callback.
function yieldToMain () {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
Manfaat fungsi yieldToMain()
adalah Anda dapat melakukan await
dalam fungsi async
apa pun. Berdasarkan contoh sebelumnya, Anda dapat membuat array fungsi untuk dijalankan, dan menghasilkan thread utama setelah masing-masing berjalan:
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();
}
}
Hasilnya adalah tugas yang sebelumnya monolitik kini dipecah menjadi tugas terpisah.
API penjadwal khusus
setTimeout
adalah cara yang efektif untuk membagi tugas, tetapi dapat memiliki kelemahan: saat Anda menyerahkan ke thread utama dengan menunda kode untuk dijalankan dalam tugas berikutnya, tugas tersebut akan ditambahkan ke akhir antrean.
Jika mengontrol semua kode di halaman, Anda dapat membuat penjadwal sendiri dengan kemampuan untuk memprioritaskan tugas, tetapi skrip pihak ketiga tidak akan menggunakan penjadwal Anda. Akibatnya, Anda tidak dapat memprioritaskan pekerjaan di lingkungan tersebut. Anda hanya dapat membaginya, atau secara eksplisit memberikan interaksi pengguna.
API penjadwal menawarkan fungsi postTask()
yang memungkinkan penjadwalan tugas yang lebih terperinci, dan merupakan salah satu cara untuk membantu browser memprioritaskan pekerjaan sehingga tugas dengan prioritas rendah akan menghasilkan thread utama. postTask()
menggunakan promise, dan menerima salah satu dari tiga setelan priority
:
'background'
untuk tugas dengan prioritas terendah.'user-visible'
untuk tugas dengan prioritas sedang. Ini adalah default jika tidak adapriority
yang ditetapkan.'user-blocking'
untuk tugas penting yang perlu dijalankan dengan prioritas tinggi.
Ambil kode berikut sebagai contoh, dengan postTask()
API digunakan untuk menjalankan tiga tugas dengan prioritas setinggi mungkin, dan dua tugas lainnya dengan prioritas 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 sedemikian rupa sehingga tugas yang diprioritaskan browser—seperti interaksi pengguna—dapat berjalan sesuai kebutuhan.
Ini adalah contoh sederhana tentang cara penggunaan postTask()
. Anda dapat membuat instance objek TaskController
yang berbeda yang dapat berbagi prioritas antartugas, termasuk kemampuan untuk mengubah prioritas untuk instance TaskController
yang berbeda sesuai kebutuhan.
Hasil bawaan dengan kelanjutan menggunakan scheduler.yield()
API
scheduler.yield()
adalah API yang dirancang khusus untuk menghasilkan thread utama di browser. Penggunaannya menyerupai fungsi yieldToMain()
yang ditunjukkan sebelumnya dalam panduan 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 kode ini menggunakan
await scheduler.yield()
, bukan yieldToMain()
.
Manfaat scheduler.yield()
adalah kelanjutan, yang berarti jika Anda menghasilkan di tengah kumpulan tugas, tugas terjadwal lainnya akan dilanjutkan dalam urutan yang sama setelah titik hasil. Tindakan ini akan mencegah kode dari skrip pihak ketiga mengganggu urutan eksekusi kode Anda.
Jangan gunakan isInputPending()
isInputPending()
API memberikan cara untuk memeriksa apakah pengguna telah mencoba berinteraksi dengan halaman dan hanya memberikan hasil jika input tertunda.
Ini memungkinkan JavaScript melanjutkan jika tidak ada input yang tertunda, bukan menghasilkan dan berakhir di bagian 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 telah meningkat, terutama dengan diperkenalkannya INP. Kami tidak lagi merekomendasikan penggunaan API ini, dan sebagai gantinya merekomendasikan untuk menghasilkan terlepas dari apakah input tertunda atau tidak karena sejumlah alasan:
isInputPending()
dapat salah menampilkanfalse
meskipun pengguna telah berinteraksi dalam situasi tertentu.- Input bukanlah satu-satunya kasus di mana tugas harus menghasilkan. Animasi dan pembaruan antarmuka pengguna reguler lainnya juga sama pentingnya untuk menyediakan halaman web yang responsif.
- API hasil yang lebih komprehensif telah diperkenalkan untuk mengatasi masalah yang menghasilkan, seperti
scheduler.postTask()
danscheduler.yield()
.
Kesimpulan
Mengelola tugas memang sulit, tetapi melakukannya akan memastikan halaman Anda merespons interaksi pengguna dengan lebih cepat. Tidak ada satu saran pun untuk mengelola dan memprioritaskan tugas, melainkan sejumlah teknik yang berbeda. Untuk mengulang, berikut adalah hal-hal utama yang perlu dipertimbangkan saat mengelola tugas:
- Menuju thread utama untuk tugas penting yang dihadapi pengguna.
- Prioritaskan tugas dengan
postTask()
. - Pertimbangkan untuk bereksperimen dengan
scheduler.yield()
. - Terakhir, lakukan pekerjaan sesedikit mungkin dalam fungsi Anda.
Dengan satu atau beberapa alat ini, Anda akan dapat menyusun pekerjaan dalam aplikasi sehingga memprioritaskan kebutuhan pengguna, sekaligus memastikan bahwa pekerjaan yang kurang penting tetap dapat diselesaikan. Hal ini akan menciptakan pengalaman pengguna yang lebih baik, yang lebih responsif dan lebih menyenangkan untuk digunakan.
Terima kasih khusus kepada Philip Walton atas pemeriksaan teknisnya terhadap panduan ini.
Gambar thumbnail yang diambil dari Unsplash, atas izin Amirali Mirhashemian.