JAVASCRIPT EVENT-LOOP WEB-PERFORMANCE FRONTEND-OPTIMIZATION UI-UX RESPONSIVENESS BROWSER ASYNC-PROGRAMMING MICROTASKS MACROTASKS PERFORMANCE-OPTIMIZATION MAIN-THREAD

Mengelola Prioritas Tugas dan Responsivitas UI dengan Microtasks & Macrotasks: Jurus Rahasia Event Loop Lanjutan

⏱️ 10 menit baca
👨‍💻

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:

💡 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:

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:

⚠️ Aturan Emas Event Loop: Setelah Call Stack kosong, Event Loop akan:

  1. Mengambil dan mengeksekusi SEMUA Microtasks yang ada di Microtask Queue, satu per satu, sampai antrean Microtask kosong.
  2. Setelah Microtask Queue kosong, baru browser bisa melakukan rendering UI (jika ada perubahan yang perlu dirender).
  3. 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:

Kekurangan:

🎯 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:

  1. Klik tombol memicu event handler (Macrotask).
  2. count++ dan updateUI() dijalankan secara sinkron.
  3. Promise.resolve().then() menjadwalkan callback-nya sebagai Microtask.
  4. Event handler selesai, Call Stack kosong.
  5. Event Loop segera memeriksa Microtask Queue dan menemukan callback Promise.then().
  6. Callback Promise.then() dijalankan.
  7. 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)