Javascript

Krisis Performa Diam-diam: Kelaparan Microtask Queue di SPA Modern

Kholil · 24 Apr 2026 · 4 min read · 1 views
Krisis Performa Diam-diam: Kelaparan Microtask Queue di SPA Modern

Microtask queue starvation adalah silent killer di SPA modern. Pelajari cara mengidentifikasi dan mengatasi masalah performa yang jarang dibicarakan ini.

Pengantar: Problem yang Sering Terlewatkan

Dalam dunia pengembangan JavaScript modern, kita sering fokus pada masalah besar seperti bundle size, rendering performance, dan state management. Namun, ada sebuah silent killer yang jarang dibicarakan: microtask queue starvation. Masalah ini dapat menyebabkan aplikasi Single Page Application (SPA) Anda terasa "macet" tanpa alasan yang jelas, membuat pengguna frustrasi meskipun CPU usage masih rendah.

Mari kita gali lebih dalam tentang fenomena ini dan bagaimana cara mengidentifikasi serta mengatasinya.

Apa Itu Microtask Queue?

Sebelum memahami starvation, kita perlu tahu apa itu microtask queue. JavaScript memiliki event loop yang mengelola dua jenis task: macrotask (seperti setTimeout, setInterval, I/O operations) dan microtask (seperti Promise callbacks, MutationObserver, queueMicrotask).

Urutan eksekusi event loop adalah sebagai berikut:

  1. Eksekusi synchronous code
  2. Kosongkan seluruh microtask queue
  3. Jalankan satu macrotask
  4. Kembali ke langkah 2

Poin krusial di sini adalah bahwa microtask queue harus kosong sepenuhnya sebelum browser dapat melakukan rendering atau menjalankan macrotask berikutnya. Ini adalah akar masalah kami.

Bagaimana Starvation Terjadi?

Microtask queue starvation terjadi ketika ada aliran Promise atau microtask yang tidak pernah berhenti, menciptakan kondisi di mana browser tidak pernah mendapat kesempatan untuk:

  • Melakukan rendering atau repaint layar
  • Menjalankan event handler (click, scroll, input)
  • Menjalankan callback setTimeout/setInterval
  • Memproses user input

Hasilnya adalah UI yang beku atau responsif sangat lambat, meskipun tidak ada blocking operation yang jelas. Mari lihat contoh kode yang dapat memicu masalah ini:

// Kode yang sangat bermasalah
function infiniteMicrotasks() {
  Promise.resolve()
    .then(() => {
      console.log('Microtask executed');
      infiniteMicrotasks(); // Rekursi yang menciptakan chain Promise tak terbatas
    });
}

infiniteMicrotasks();

// UI akan completely frozen di sini

Dalam contoh ini, setiap Promise yang resolve menciptakan microtask baru, dan microtask baru itu menciptakan Promise lagi. Browser tidak pernah mendapat kesempatan untuk keluar dari microtask queue.

Skenario Nyata: Di Mana Ini Terjadi?

Microtask starvation sering terjadi dalam situasi yang tampak innocent:

1. Event Delegation dengan Promise Chain

// Framework library yang poorly implemented
element.addEventListener('click', async (e) => {
  let count = 0;
  while (count < 10000) {
    await new Promise(resolve => {
      // Setiap await menciptakan microtask baru
      process_item(count);
      resolve();
      count++;
    });
  }
});

2. Reactive Framework dengan Computed Dependencies

Framework seperti Vue 3 atau React menggunakan Promise untuk batching updates. Jika ada computed property atau selector yang terus-menerus di-trigger, microtask queue bisa tersaturasi.

3. Library Validasi Form yang Agresif

// Validasi real-time yang terlalu eager
inputElement.addEventListener('input', (e) => {
  const value = e.target.value;
  
  // Chain Promise untuk setiap validasi
  validateAsync(value)
    .then(result => validateAsync2(result))
    .then(result => validateAsync3(result))
    .then(result => validateAsync4(result))
    // ... dan seterusnya
});

Mendeteksi Microtask Starvation

Bagaimana cara tahu apakah aplikasi Anda mengalami masalah ini?

Gejala-gejala:

  • UI responsiveness rendah tapi CPU usage tidak tinggi
  • Scroll jadi choppy dan jittery
  • Input lag terasa jelas meskipun tidak ada heavy computation
  • DevTools menunjukkan event handlers menunggu lama

Debugging dengan Performance API:

// Monitor microtask execution time
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 50) {
      console.warn('Long microtask detected:', entry);
    }
  }
});

observer.observe({ entryTypes: ['measure'] });

// Atau gunakan console timing
console.time('microtask-work');
// ... kode yang ingin diukur
console.timeEnd('microtask-work');

Solusi Praktis

1. Batching dengan setImmediate atau setTimeout

Alihkan beberapa microtask ke macrotask queue untuk memberikan browser kesempatan rendering:

// Baik: Memecah pekerjaan menjadi chunks
async function processLargeDataset(items) {
  for (let i = 0; i < items.length; i += BATCH_SIZE) {
    const batch = items.slice(i, i + BATCH_SIZE);
    await processBatch(batch);
    
    // Yield ke browser untuk rendering
    await new Promise(resolve => setTimeout(resolve, 0));
  }
}

2. Gunakan scheduler.yield() (Experimental)

// API modern untuk yielding ke browser
async function processItems(items) {
  for (const item of items) {
    processItem(item);
    
    // Yield ke browser jika task scheduler tersedia
    if (navigator.scheduling?.isInputPending?.()) {
      await scheduler.yield();
    }
  }
}

3. Implementasi Task Queue Manual

// Queue yang respectful terhadap browser
class RespectfulQueue {
  constructor(concurrency = 5) {
    this.queue = [];
    this.running = 0;
    this.concurrency = concurrency;
  }
  
  async add(task) {
    return new Promise((resolve) => {
      this.queue.push({ task, resolve });
      this.process();
    });
  }
  
  async process() {
    while (this.running < this.concurrency && this.queue.length) {
      this.running++;
      const { task, resolve } = this.queue.shift();
      
      try {
        const result = await task();
        resolve(result);
      } finally {
        this.running--;
        // Yield ke browser setiap selesai task
        await new Promise(r => setTimeout(r, 0));
        this.process();
      }
    }
  }
}

Best Practices

  • Monitor microtask timing dalam development dan production
  • Break long async chains menjadi lebih pendek dengan yield points
  • Avoid creating unnecessary Promise chains dalam tight loops
  • Gunakan requestIdleCallback untuk work yang non-critical
  • Profile aplikasi secara regular dengan DevTools Performance tab

Kesimpulan

Microtask queue starvation adalah masalah performa yang subtle namun dapat merusak user experience secara signifikan. Kunci untuk mengatasi masalah ini adalah pemahaman mendalam tentang JavaScript event loop dan disiplin dalam memecah pekerjaan asynchronous menjadi chunks yang lebih kecil.

Dengan monitoring yang tepat dan strategi yielding yang baik, Anda dapat memastikan aplikasi SPA Anda tetap responsif bahkan ketika memproses jumlah data yang besar. Ingat: performa yang baik bukan hanya tentang kecepatan, tetapi juga tentang responsiveness yang konsisten.