Mengoptimalkan tugas yang berjalan lama

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.

Screenshot tugas di profliler performa Chrome DevTools. Tugas berada di bagian atas tumpukan, dengan pengendali peristiwa klik, panggilan fungsi, dan item lainnya di bawahnya. Tugas ini juga mencakup beberapa pekerjaan rendering di sisi kanan.
Tugas yang dimulai oleh pengendali peristiwa click di, ditampilkan di profiler performa Chrome DevTools.

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.

Tugas panjang di profiler performa Chrome DevTools. Bagian yang memblokir tugas (lebih dari 50 milidetik) ditandai dengan garis diagonal merah.
Tugas panjang yang ditampilkan di profiler performa Chrome. Tugas panjang ditunjukkan dengan segitiga merah di sudut tugas, dengan bagian pemblokir tugas yang diisi dengan pola garis merah diagonal.

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.

Satu tugas panjang versus tugas yang sama dipecah menjadi tugas-tugas yang lebih singkat. Tugas panjang adalah satu kotak besar, dan tugas yang dipotong-potong adalah lima kotak lebih kecil yang panjangnya bertambah sesuai dengan panjang tugas yang panjang.
Visualisasi satu tugas panjang versus tugas yang sama tersebut dipecah menjadi lima tugas yang lebih singkat.

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.

Memecah sebuah tugas dapat memfasilitasi interaksi pengguna. Di bagian atas, tugas yang panjang akan memblokir pengendali peristiwa agar tidak berjalan hingga tugas selesai. Di bagian bawah, tugas potongan memungkinkan pengendali peristiwa berjalan lebih cepat dari yang seharusnya.
Jika tugas terlalu panjang, browser tidak dapat merespons interaksi dengan cukup cepat. Memecah tugas memungkinkan interaksi tersebut terjadi lebih cepat.

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();
}
Fungsi saveSettings ditampilkan di profiler performa Chrome. Sementara fungsi tingkat atas memanggil lima fungsi lainnya, semua pekerjaan berlangsung dalam satu tugas panjang yang memblokir thread utama.
Fungsi tunggal saveSettings() yang memanggil lima fungsi. Pekerjaan dijalankan sebagai bagian dari satu tugas monolitik yang panjang.

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.

Fungsi
    saveSettings yang sama di profiler performa Chrome, sekarang dengan menghasilkan.
    Tugas sekarang dipecah menjadi lima tugas terpisah, satu untuk setiap fungsi.
Fungsi saveSettings() kini menjalankan fungsi turunannya sebagai tugas terpisah.

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.

Dukungan Browser

  • 94
  • 94
  • x

Sumber

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 ada priority 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.

Fungsi
    saveSettings ditampilkan di profiler performa Chrome, tetapi menggunakan
    postTask. postTask membagi setiap fungsi SaveSettings yang berjalan, dan
    memprioritaskannya sehingga interaksi pengguna dapat berjalan tanpa diblokir.
Saat saveSettings() berjalan, fungsi tersebut menjadwalkan panggilan fungsi individual menggunakan postTask(). Pekerjaan penting yang dihadapi pengguna dijadwalkan dengan prioritas tinggi, sedangkan pekerjaan yang tidak diketahui pengguna dijadwalkan untuk berjalan di latar belakang. Hal ini memungkinkan interaksi pengguna dijalankan dengan lebih cepat, karena pekerjaan dipecah dan diprioritaskan dengan tepat.

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().

Tiga diagram
    yang menunjukkan tugas tanpa menghasilkan, dengan hasil, serta dengan
    pemberian dan kelanjutan. Tanpa menyerah, akan ada tugas yang berjalan lama. Dengan menghasilkan,
    ada lebih banyak tugas yang lebih singkat, tetapi dapat terganggu oleh
    tugas lain yang tidak terkait. Dengan hasil dan kelanjutan, urutan eksekusi tugas yang lebih pendek akan dipertahankan.
Saat Anda menggunakan scheduler.yield(), eksekusi tugas akan melanjutkan pekerjaan bahkan setelah titik hasil.

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() dan scheduler.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.