Mengelola Prioritas Tugas dan Responsivitas UI dengan Microtasks & Macrotasks: Jurus Rahasia Event Loop Lanjutan
1. Pendahuluan
Pernahkah Anda mengalami aplikasi web yang terasa “berat”, tersendat, atau bahkan “hang” sejenak saat melakukan operasi tertentu? Tombol yang tidak responsif, animasi yang patah-patah, atau input teks yang lambat adalah tanda-tanda umum dari apa yang kita sebut sebagai “jank” di UI. Masalah ini seringkali berakar pada satu hal fundamental di JavaScript: sifat single-threaded-nya dan bagaimana ia berinteraksi dengan Event Loop.
Sebagai developer web, kita seringkali dihadapkan pada tugas-tugas yang membutuhkan komputasi berat atau manipulasi DOM yang intensif. Jika tugas-tugas ini tidak dikelola dengan baik, mereka bisa memblokir main thread browser, yang bertanggung jawab atas rendering UI, penanganan event, dan eksekusi JavaScript lainnya. Akibatnya, pengalaman pengguna menjadi buruk.
Artikel ini akan membawa Anda menggali lebih dalam ke jantung JavaScript: Event Loop, dengan fokus khusus pada dua jenis tugas yang seringkali disalahpahami: Microtasks dan Macrotasks. Kita akan memahami perbedaan fundamental keduanya dan, yang lebih penting, bagaimana Anda bisa secara strategis memanfaatkan mereka untuk memecah pekerjaan berat, mengelola prioritas eksekusi kode, dan pada akhirnya, membangun aplikasi web yang super responsif dan bebas jank.
Siapkah Anda untuk menguasai jurus rahasia Event Loop dan membuat UI Anda selalu mulus? Mari kita mulai!
2. Sekilas Tentang Event Loop: Jantung Asinkron JavaScript
Sebelum kita menyelam ke Microtasks dan Macrotasks, mari kita ingat kembali esensi Event Loop. 📌 JavaScript, di browser maupun Node.js, adalah bahasa single-threaded. Ini berarti ia hanya bisa melakukan satu hal pada satu waktu.
Lalu, bagaimana kita bisa melakukan operasi asinkron seperti fetching data dari API, setTimeout, atau menangani klik tombol tanpa memblokir seluruh aplikasi? Jawabannya ada pada Event Loop.
Secara sederhana, Event Loop adalah mekanisme yang memungkinkan JavaScript mengelola eksekusi kode asinkron. Ia bekerja dengan beberapa komponen utama:
- Call Stack: Tempat di mana kode JavaScript sinkronus dieksekusi. Ketika sebuah fungsi dipanggil, ia masuk ke stack, dan ketika selesai, ia keluar dari stack.
- Web APIs (atau Node.js APIs): Lingkungan browser menyediakan API seperti
setTimeout,fetch,DOM events, dll. Ketika Anda memanggil fungsi-fungsi ini, browser akan menangani tugas asinkronnya di luar Call Stack. - Callback Queue (atau Task Queue / Macrotask Queue): Setelah Web API menyelesaikan tugasnya (misalnya,
setTimeoutselesai menunggu, atau datafetchsudah diterima), callback yang terkait akan dimasukkan ke dalam antrean ini. - Microtask Queue: Ini adalah antrean terpisah dengan prioritas lebih tinggi (akan kita bahas lebih lanjut).
- Event Loop: Ini adalah “penjaga gerbang” yang terus-menerus memantau Call Stack. Jika Call Stack kosong, Event Loop akan mengambil tugas dari antrean (pertama Microtask Queue, lalu Callback Queue) dan mendorongnya ke Call Stack untuk dieksekusi.
💡 Intinya: JavaScript selalu berusaha menyelesaikan semua kode di Call Stack terlebih dahulu. Setelah itu, ia akan melihat antrean tugas asinkron.
3. Microtasks vs. Macrotasks: Siapa yang Lebih Dulu?
Ini adalah inti dari strategi kita. Event Loop tidak hanya memiliki satu antrean tugas. Ada dua jenis antrean utama dengan prioritas yang berbeda:
Macrotasks (atau Tasks)
Macrotasks adalah tugas-tugas “besar” yang mencakup eksekusi kode secara keseluruhan, rendering UI, dan interaksi event. Contoh Macrotasks:
setTimeout()setInterval()requestAnimationFrame()(meskipun ini lebih spesifik untuk rendering)- I/O (membaca file, network requests yang menyelesaikan callback-nya)
- Event-event DOM (klik, scroll, input, dll.)
- Parsing HTML, rendering UI
Ketika Call Stack kosong, Event Loop akan mengambil satu Macrotask dari Macrotask Queue dan menjalankannya sampai selesai. Setelah Macrotask itu selesai, Event Loop tidak langsung mengambil Macrotask berikutnya. Ia akan berhenti sejenak untuk memeriksa antrean lain…
Microtasks
Microtasks adalah tugas-tugas “kecil” yang memiliki prioritas lebih tinggi daripada Macrotasks. Mereka dirancang untuk dijalankan sesegera mungkin setelah kode sinkronus saat ini selesai, dan sebelum browser melakukan rendering ulang UI atau mengambil Macrotask berikutnya.
Contoh Microtasks:
Promise.then(),Promise.catch(),Promise.finally()async/await(karenaawaitsecara internal menggunakan Promise)queueMicrotask()MutationObserver(mengamati perubahan DOM)
⚠️ Aturan Emas Event Loop: Setelah Call Stack kosong, Event Loop akan:
- Mengambil dan mengeksekusi SEMUA Microtasks yang ada di Microtask Queue, satu per satu, sampai antrean Microtask kosong.
- Setelah Microtask Queue kosong, baru browser bisa melakukan rendering UI (jika ada perubahan yang perlu dirender).
- Setelah rendering, Event Loop akan mengambil SATU Macrotask dari Macrotask Queue dan memulai siklus lagi.
Diagram sederhananya:
Call Stack (kosong)
-> Microtask Queue (eksekusi semua)
-> Render UI (jika ada)
-> Macrotask Queue (eksekusi satu)
-> Ulangi
Memahami urutan ini adalah kunci untuk mengelola responsivitas UI.
4. Jurus Rahasia: Memecah Tugas Panjang dengan Macrotasks (Strategi setTimeout(0))
Salah satu penyebab utama jank adalah ketika ada fungsi JavaScript sinkronus yang berjalan terlalu lama di main thread. Bayangkan Anda punya loop yang mengolah ribuan data atau melakukan perhitungan kompleks. Selama loop itu berjalan, UI akan “beku” karena browser tidak bisa merespons input pengguna atau melakukan rendering.
Problem: Blocking Main Thread
function heavyComputation() {
console.log("Mulai komputasi berat...");
let sum = 0;
for (let i = 0; i < 1_000_000_000; i++) {
sum += i;
}
console.log("Komputasi berat selesai:", sum);
document.getElementById('status').innerText = 'Komputasi selesai!';
}
document.getElementById('start-button').addEventListener('click', () => {
document.getElementById('status').innerText = 'Mulai komputasi...';
heavyComputation();
});
Jika Anda menjalankan kode di atas, saat heavyComputation() berjalan, tombol atau elemen lain di halaman tidak akan bisa diklik, dan teks status mungkin tidak akan terupdate sampai seluruh komputasi selesai. UI akan beku.
Solusi: Memecah Tugas dengan setTimeout(0)
Kita bisa memecah komputasi berat ini menjadi “shift” yang lebih kecil menggunakan setTimeout(0). Dengan nilai timeout 0 (atau nilai kecil lainnya), kita secara efektif memberitahu browser: “Jalankan fungsi ini sebagai Macrotask berikutnya, setelah Call Stack kosong dan setelah semua Microtasks (jika ada) selesai, dan berikan kesempatan browser untuk me-render UI di antaranya.”
function chunkedHeavyComputation(index, totalIterations, chunkSize, currentSum) {
const start = index;
const end = Math.min(index + chunkSize, totalIterations);
for (let i = start; i < end; i++) {
currentSum += i;
}
document.getElementById('progress').innerText = `Progress: ${Math.floor((end / totalIterations) * 100)}%`;
if (end < totalIterations) {
// Jadwalkan chunk berikutnya sebagai Macrotask baru
setTimeout(() => {
chunkedHeavyComputation(end, totalIterations, chunkSize, currentSum);
}, 0);
} else {
console.log("Komputasi berat selesai:", currentSum);
document.getElementById('status').innerText = 'Komputasi selesai!';
document.getElementById('result').innerText = `Hasil: ${currentSum}`;
}
}
document.getElementById('start-button-chunked').addEventListener('click', () => {
document.getElementById('status').innerText = 'Mulai komputasi chunked...';
document.getElementById('result').innerText = '';
const totalIterations = 1_000_000_000;
const chunkSize = 1_000_000; // Ukuran setiap "shift"
chunkedHeavyComputation(0, totalIterations, chunkSize, 0);
});
✅ Keuntungan:
- Responsivitas UI Terjaga: Browser mendapatkan kesempatan untuk memproses event dan me-render UI di antara setiap “chunk” komputasi. Aplikasi tidak akan terasa beku.
- Pengalaman Pengguna Lebih Baik: Anda bisa menampilkan indikator progres yang benar-benar update secara real-time.
❌ Kekurangan:
- Latensi Tambahan: Ada sedikit overhead karena setiap chunk dijadwalkan ulang.
- Tidak Ada Jaminan Timing:
setTimeout(0)tidak berarti “jalankan segera”. Itu berarti “jalankan sebagai Macrotask berikutnya yang tersedia”. Browser mungkin punya Macrotask lain atau Microtasks yang harus diselesaikan dulu.
🎯 Kapan Menggunakannya? Saat Anda memiliki tugas komputasi berat yang harus dijalankan di main thread (misalnya, karena manipulasi DOM langsung atau akses ke state yang tidak bisa di-serialize ke Web Worker), dan Anda ingin menjaga UI tetap responsif.
5. Memprioritaskan Update UI dengan Microtasks (Strategi Promise.resolve().then())
Microtasks memberikan kita kemampuan untuk menjalankan kode segera setelah Call Stack kosong, tetapi sebelum browser melakukan rendering atau mengambil Macrotask berikutnya. Ini sangat berguna untuk skenario di mana Anda memiliki beberapa update atau kalkulasi yang saling terkait dan harus selesai dalam satu “tick” Event Loop yang sama, tanpa intervensi rendering browser di tengah-tengah.
Problem: Intervensi Rendering atau Macrotask di Tengah Update yang Terkait
Bayangkan Anda memiliki serangkaian update DOM yang harus dilakukan, dan Anda ingin memastikan semua kalkulasi terkait selesai sebelum browser me-render ulang halaman. Jika Anda hanya menggunakan setTimeout(0), ada kemungkinan rendering atau Macrotask lain masuk di antara update Anda.
Solusi: Mengelompokkan dengan Promise.resolve().then() atau queueMicrotask()
let count = 0;
function updateUI() {
// Simulasikan beberapa update DOM yang saling terkait
document.getElementById('box').style.width = `${100 + count * 10}px`;
document.getElementById('box').style.height = `${100 + count * 10}px`;
document.getElementById('box').innerText = `Ukuran: ${100 + count * 10}`;
console.log(`Update DOM #${count}`);
}
document.getElementById('microtask-button').addEventListener('click', () => {
count++;
console.log("Tombol diklik. Menjadwalkan microtask...");
// Update pertama (sinkronus)
updateUI();
// Jadwalkan tugas lain sebagai microtask
Promise.resolve().then(() => {
// Ini akan dijalankan setelah updateUI() selesai,
// tapi sebelum browser me-render ulang atau Macrotask lain.
console.log("Microtask dijalankan! Melakukan kalkulasi final...");
const finalSize = parseInt(document.getElementById('box').style.width);
document.getElementById('final-status').innerText = `Final size calculated in microtask: ${finalSize}`;
});
console.log("Kode event handler selesai.");
});
Dalam contoh di atas:
- Klik tombol memicu event handler (Macrotask).
count++danupdateUI()dijalankan secara sinkron.Promise.resolve().then()menjadwalkan callback-nya sebagai Microtask.- Event handler selesai, Call Stack kosong.
- Event Loop segera memeriksa Microtask Queue dan menemukan callback
Promise.then(). - Callback
Promise.then()dijalankan. - Baru setelah Microtask Queue kosong, browser bisa me-render perubahan DOM yang dilakukan oleh
updateUI().
Ini memastikan bahwa semua kalkulasi atau update yang sangat terkait dan perlu selesai dalam satu “atom” eksekusi (sebelum rendering) dapat dikelompokkan.
💡 Tips: queueMicrotask() adalah API yang lebih baru dan lebih eksplisit untuk menjadwalkan microtask tanpa harus membuat Promise.
queueMicrotask(() => {
console.log("Ini juga microtask!");
});
⚠️ Hati-hati dengan Microtask Starvation: Jika Anda menjadwalkan terlalu banyak Microtasks dalam satu “tick” Event Loop, Anda bisa secara tidak sengaja menunda rendering UI dan eksekusi Macrotasks lainnya. Ini paradoksnya bisa membuat UI terasa tidak responsif, meskipun Microtasks dijalankan dengan prioritas tinggi. Pastikan Microtasks Anda cepat dan efisien.
6. Studi Kasus & Best Practices
Memahami Microtasks dan Macrotasks bukan hanya teori, tetapi kunci untuk mengoptimalkan kinerja aplikasi Anda di berbagai skenario.
a. Batching DOM Updates untuk Animasi Halus
Ketika Anda melakukan banyak perubahan gaya atau struktur DOM yang saling terkait, menjadwalkannya dalam satu frame rendering sangat penting untuk animasi yang halus.
function animateElement(element, targetX, targetY) {
let currentX = 0;
let currentY = 0;
const duration = 1000; // ms
const startTime = performance.now();
function animateFrame() {
const elapsed = performance.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
// Kalkulasi posisi baru
currentX = targetX * progress;
currentY = targetY * progress;
// Menjadwalkan update DOM sebagai Microtask (opsional, jika perlu kalkulasi sebelum render)
// atau langsung di requestAnimationFrame (Macrotask yang disinkronkan dengan render)
// Untuk update visual, requestAnimationFrame adalah yang terbaik karena disinkronkan dengan refresh rate monitor.
element.style.transform = `translate(${currentX}px, ${currentY}px)`;
if (progress < 1) {
requestAnimationFrame(animateFrame); // Jadwalkan frame berikutnya
}
}
requestAnimationFrame(animateFrame);
}
const myBox = document.getElementById('animated-box');
document.getElementById('start-animation').addEventListener('click', () => {
animateElement(myBox, 200, 100);
});
✅ requestAnimationFrame adalah Macrotask khusus yang dioptimalkan untuk update visual. Browser menjamin callback-nya akan dipanggil tepat sebelum rendering berikutnya, memastikan animasi sehalus mungkin.
b. Memecah Pekerjaan Berat: Web Workers vs. setTimeout(0)
- Web Workers: Pilihan terbaik untuk komputasi yang *sangat