Task Queue vs Microtask Queue: Memahami Urutan Eksekusi Javascript
Pernah merasa bingung mengapa setTimeout
tidak langsung dieksekusi meskipun sudah dipanggil dengan timeout=0? Atau mungkin kamu sering menggunakan Promise tetapi belum sepenuhnya memahami bagaimana JavaScript menangani operasi seperti ini?
Di JavaScript, ada dua mekanisme penting yang mempengaruhi urutan eksekusi operasi asynchronous: task queue dan microtask queue.
Mari kita telusuri lebih dalam bagaimana masing-masing mekanisme ini berfungsi dan bagaimana mereka memengaruhi urutan eksekusi.
Task Queue: Antrian Utama Event Loop
Task Queue adalah antrian utama yang digunakan oleh Web API untuk mengirimkan callback. Ketika kamu menjalankan setTimeout
atau mengirimkan AJAX request, JavaScript akan menaruh callback-nya ke dalam task queue.
Contoh task yang masuk ke task queue:
Callback dari
setTimeout
Callback dari
setInterval
Request AJAX
Nah, yang penting diingat: event loop akan memproses task queue hanya setelah semua task di call stack selesai. Tapi sebelum itu, event loop akan memprioritaskan yang ada di microtask queue dulu. Lho, apa itu microtask queue?
Microtask Queue: Antrian Yang Di Prioritaskan
Microtask Queue menampung task-task dengan ukuran lebih kecil yang diprioritaskan untuk dieksekusi sebelum task di task queue (juga dikenal sebagai macro task). Ini berarti microtask selalu didahulukan jika call stack sudah kosong, bahkan sebelum task lain dijalankan.
Karakteristik utama dari microtask queue:
Semua task di microtask queue akan dieksekusi terlebih dahulu sebelum task di task queue.
Task ini biasanya melibatkan operasi yang harus segera diselesaikan setelah eksekusi kode saat ini.
Contoh microtask:
Promise (
.then
atau.catch
)API
fetch
MutationObserver
Kamu juga bisa menambahkan microtask secara manual menggunakan
queueMicrotask()
Berikut adalah ilustrasi yang sangat bagus dari sebuah artikel yang menjelaskan microtask dan macrotask dari situs dev.to oleh Lydia Hallie.
Disini kita bisa lihat secara langsung bahwa event loop akan memproses semua task yang ada pada microtask sampai selesai. Setelah itu event loop akan lanjut memproses task tang ada pada task queue (macrotask).
Kapan Harus Menggunakan Microtask?
Karena task di microtask queue diprioritaskan, kamu mungkin berpikir microtask selalu lebih baik. Tapi tidak juga! Ada beberapa situasi tertentu di mana microtask sebaiknya digunakan, dan ada juga ketika macro task (seperti setTimeout
) lebih tepat.
1. Prioritas Eksekusi
Microtask ideal ketika kamu perlu menjalankan task setelah kode di call stack selesai, tetapi sebelum browser melakukan rendering atau memproses event dari user. Beberapa kasus umum:
Operasi yang bergantung pada hasil kode sebelumnya. Misalnya, jika kamu punya sebuah promise yang harus menyelesaikan eksekusi sebelum user bisa melakukan interaksi lagi.
Task yang tidak boleh memblokir UI atau penanganan event. Dengan microtask, kamu bisa memastikan UI tetap responsif karena eksekusi task dilakukan di sela-sela event user dan rendering UI.
2. Batching Process
Microtask sangat berguna untuk mengelompokkan beberapa operasi asinkron sehingga lebih efisien. Alih-alih menjalankan task satu per satu, kamu bisa mengelompokkannya dalam satu batch sehingga:
Operasi-operasi dieksekusi bersamaan.
Mengurangi beban pada event loop dan menghindari penundaan yang terlalu lama.
Membuat aplikasi lebih cepat dan lebih responsif.
Contoh Praktis: Mengambil Data dari API dan Memperbarui UI
Mari kita lihat sebuah contoh sederhana. Misalkan kamu mengambil data dari API dan ingin memperbarui UI setelah datanya diterima.
javascriptCopy codefetch('/api/data')
.then(response => response.json())
.then(data => updateUI(data));
Di contoh ini, kita menggunakan Promise untuk menunggu respons dari server:
Promise secara otomatis menggunakan microtask queue untuk menjalankan callback.
Pemrosesan data dan pembaruan UI terjadi segera setelah data diterima, sebelum browser melakukan rendering.
Ini memastikan pembaruan UI terjadi dengan cepat dan mulus.
Dampak Penggunaan Microtask terhadap Performa
Meski microtask memiliki prioritas tinggi, penggunaan yang tidak tepat dapat mengganggu performa aplikasi. Terlalu banyak microtask yang menumpuk bisa membuat aplikasi terasa lambat karena mereka akan memblokir task di task queue yang juga penting, seperti pembaruan UI atau penanganan event user.
Berikut beberapa tips agar penggunaan microtask tidak merugikan performa:
Jangan overuse microtask. Gunakan hanya ketika kamu benar-benar butuh operasi berprioritas tinggi yang harus segera dijalankan.
Batching task dengan bijak. Jika kamu memiliki beberapa task asinkron, kelola mereka dalam satu batch menggunakan microtask, tetapi jangan membebani event loop terlalu banyak sekaligus.
Pantau dampak pada UI. Selalu pastikan bahwa penggunaan microtask tidak menyebabkan masalah seperti stuttering pada UI atau membuat interaksi user terasa lambat.
Contoh Kasus: Pertanyaan Interview Yang Sering Ditanyakan
Coba tebak apa output dari program berikut? sebelum lanjut scroll kebawah coba tuliskan dulu jawaban mu di sebuah kertas dan nanti kita akan cocok kan dengan penjelasn yang ada dibawah kode ini.
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
});
new Promise((resolve, reject) => {
console.log('New Promise 1');
resolve();
}).then(() => {
console.log('Promise 2');
});
console.log('End');
1. console.log('Start')
Ini adalah synchronous task, sehingga langsung dieksekusi begitu kode dijalankan. Maka, output pertama yang akan muncul di console adalah:
Start
2. setTimeout(() => { console.log('Timeout 1'); }, 0)
setTimeout
adalah asynchronous task, dan callback-nya akan dimasukkan ke task queue (macro task), tapi hanya setelah semua synchronous task selesai dieksekusi. Karena waktu delay yang diberikan adalah 0
, callback akan dijalankan sesegera mungkin, tetapi hanya setelah semua microtask selesai.
Untuk saat ini, callback ini akan menunggu di task queue sampai event loop siap memproses task queue setelah semua microtask selesai. Jadi, fungsi callback dari setTimeout
ini belum bisa diekseskusi dan akan menunggu call stack menyelesaikan synchronous task dan task yang ada pada antrian microtask.
3. Promise.resolve().then(() => { console.log('Promise 1'); });
Promise.resolve()
segera memanggil callback then()
, yang akan dijadwalkan sebagai microtask. Microtask diprioritaskan lebih tinggi daripada macro task (seperti setTimeout
diatas), jadi callback ini akan diproses setelah synchronous code selesai, namun sebelum callback setTimeout
dari task queue.
Saat ini, microtask ini menunggu dalam microtask queue sampai synchronous task selesai.
4. new Promise((resolve, reject) => { console.log('New Promise 1'); resolve(); }).then(() => { console.log('Promise 2'); });
Kode dalam constructor
new Promise
dieksekusi secara langsung (sync) saat Promise dibuat. Jadi, perintah synchronousconsole.log('New Promise 1')
akan langsung dieksekusi:New Promise 1
Setelah itu, karena
resolve()
dipanggil, callbackthen()
akan ditambahkan ke microtask queue. Sama sepertiPromise.resolve()
, callback ini akan dieksekusi setelah semua synchronous task selesai.
Jadi, sekarang ada dua callback microtask dalam antrian (dari dua promise), menunggu untuk dieksekusi setelah synchronous code selesai.
5. console.log('End')
Ini adalah perintah synchronous lainnya, jadi langsung dieksekusi setelah semua kode di atas. Output di console selanjutnya adalah:
End
Sekarang, setelah semua synchronous code selesai dan call stack kosong, event loop akan memproses microtask queue.
Jadi output dari kedua Promise.then()
callback adalah:
Promise 1
Promise 2
Terakhir, event loop akan memproses callback di task queue, yakni console.log('Timeout 1')
.
Yang menarik disini adalah jika anda mengatur waktu timeout=0, itu maksudnya adalah callback akan dikirimkan ke task queue setelah 0ms. Bukan callback akan di eksekusi setelah 0ms.
Jadi meskipun timeout-nya 0ms, eksekusi callback bisa tertunda, misalnya hingga 100ms, tergantung pada panjangnya antrian task yang harus diproses lebih dulu.
Semoga jelas ya perbedaannya.
Timeout 1
Jadi, output akhir di console adalah:
Start
New Promise 1
End
Promise 1
Promise 2
Timeout 1
Dengan mengerti urutan eksekusi synchronous tasks, microtasks (seperti Promise
), dan macro tasks (seperti setTimeout
), kita akan dapat mengoptimalkan performa aplikasi dan memastikan kode berjalan sesuai harapan.