JAVASCRIPT EVENT-LOOP FRONTEND WEB-PERFORMANCE BROWSER ASYNCHRONOUS USER-EXPERIENCE OPTIMIZATION CONCURRENCY

JavaScript Event Loop: Memahami Otak di Balik Aplikasi Web Responsif Anda

⏱️ 10 menit baca
👨‍💻

1. Pendahuluan

Pernahkah Anda menggunakan aplikasi web yang tiba-tiba “membeku” saat Anda mencoba berinteraksi dengannya? Atau mungkin Anda melihat animasi yang tersendat-sendat, atau tombol yang tidak merespons klik Anda? Frustrasi, bukan? Di balik layar, seringkali masalah ini berakar pada bagaimana JavaScript mengelola tugas-tugasnya.

Di dunia web modern yang serba interaktif, responsivitas adalah kunci pengalaman pengguna yang baik. Aplikasi harus mampu menangani berbagai event—dari klik pengguna, fetching data dari server, hingga animasi kompleks—tanpa mengorbankan kelancaran antarmuka. Inilah mengapa memahami JavaScript Event Loop menjadi sangat krusial bagi setiap developer web.

Event Loop adalah jantung dari model konkurensi JavaScript. Ia adalah mekanisme yang memungkinkan JavaScript, meskipun bersifat single-threaded (hanya bisa melakukan satu hal pada satu waktu), untuk melakukan operasi non-blocking I/O dan menangani event asynchronous. Tanpa Event Loop, setiap operasi yang memakan waktu lama akan langsung membekukan seluruh aplikasi Anda.

Dalam artikel ini, kita akan menyelam lebih dalam ke Event Loop: apa itu, bagaimana cara kerjanya, komponen-komponen utamanya, dan yang terpenting, bagaimana Anda bisa memanfaatkannya untuk membangun aplikasi web yang lebih cepat, lebih responsif, dan memberikan pengalaman pengguna yang mulus.

Mari kita mulai!

2. Kenapa Kita Butuh Event Loop? JavaScript itu Single-Threaded!

Untuk memahami pentingnya Event Loop, kita harus kembali ke dasar: sifat JavaScript. Secara fundamental, JavaScript adalah bahasa pemrograman single-threaded. Ini berarti ia hanya memiliki satu “thread” eksekusi, yang bertanggung jawab untuk menjalankan semua kode Anda.

Bayangkan Anda adalah seorang koki di dapur. Anda hanya bisa memotong bawang, atau merebus air, atau menggoreng lauk pada satu waktu. Anda tidak bisa melakukan ketiga hal itu secara bersamaan. Jika ada satu tugas yang memakan waktu sangat lama (misalnya, merebus sup selama satu jam), maka semua tugas lain harus menunggu sampai sup itu selesai direbus.

Dalam konteks browser, “tugas” ini bisa berupa:

Jika JavaScript adalah single-threaded murni tanpa mekanisme khusus, ketika Anda melakukan operasi yang memakan waktu (misalnya, mengambil data besar dari API), browser akan “membeku”. Anda tidak bisa mengklik tombol lain, animasi akan berhenti, dan halaman tidak akan di-render ulang sampai operasi fetching data selesai. Tentu saja, ini adalah pengalaman pengguna yang buruk.

Solusinya adalah model konkurensi asynchronous yang didukung oleh Event Loop. Model ini memungkinkan JavaScript untuk “mendelegasikan” tugas-tugas yang memakan waktu ke lingkungan di sekitarnya (browser atau Node.js) dan terus menjalankan tugas lain, kemudian “kembali” ke tugas yang didelegasikan setelah selesai.

3. Komponen Utama Event Loop

Event Loop bukanlah satu entitas tunggal, melainkan sebuah orkestrator yang menghubungkan beberapa komponen kunci dalam ekosistem JavaScript di browser:

a. Call Stack

📌 Call Stack adalah tempat di mana kode JavaScript Anda dieksekusi secara sinkron. Setiap fungsi yang dipanggil akan “didorong” ke stack, dan setelah selesai, akan “dikeluarkan” dari stack. Ini adalah struktur data LIFO (Last-In, First-Out).

function kedua() {
  console.log('Fungsi Kedua');
}

function pertama() {
  console.log('Fungsi Pertama');
  kedua(); // Panggil fungsi kedua
  console.log('Fungsi Pertama Selesai');
}

pertama(); // Panggil fungsi pertama
console.log('Selesai Eksekusi');

/*
Output:
Fungsi Pertama
Fungsi Kedua
Fungsi Pertama Selesai
Selesai Eksekusi
*/

Saat pertama() dipanggil, ia masuk ke Call Stack. Di dalamnya, kedua() dipanggil dan masuk ke Call Stack di atas pertama(). Setelah kedua() selesai, ia keluar, dan pertama() melanjutkan eksekusi hingga selesai, lalu keluar.

b. Web APIs

Web APIs adalah API yang disediakan oleh lingkungan browser (bukan bagian dari JavaScript engine itu sendiri) untuk melakukan tugas-tugas yang bersifat asynchronous. Contohnya termasuk:

Ketika JavaScript memanggil salah satu Web APIs ini, ia “mendelegasikan” tugas tersebut ke browser. Browser akan menjalankan tugas itu di luar Call Stack JavaScript utama, sehingga JavaScript bisa terus menjalankan kode lain.

c. Callback Queue (Macrotask Queue / Task Queue)

Setelah sebuah tugas yang didelegasikan ke Web APIs selesai, callback-nya (fungsi yang harus dijalankan setelah tugas selesai) tidak langsung masuk ke Call Stack. Sebaliknya, ia akan masuk ke Callback Queue (sering disebut juga Macrotask Queue atau Task Queue). Ini adalah antrean FIFO (First-In, First-Out) yang menyimpan callback-callback yang siap untuk dieksekusi.

Contoh: callback dari setTimeout, setInterval, DOM events, I/O (seperti membaca file).

d. Microtask Queue

Selain Callback Queue, ada juga Microtask Queue. Ini adalah antrean lain untuk tugas asynchronous, tetapi dengan prioritas yang lebih tinggi daripada Callback Queue.

Contoh: callback dari Promise.then(), Promise.catch(), Promise.finally(), async/await (yang secara internal menggunakan Promise), dan MutationObserver.

e. Event Loop

🎯 Event Loop adalah orkestrator yang terus-menerus memantau Call Stack dan kedua antrean (Microtask Queue dan Callback Queue). Tugas utamanya adalah:

  1. Memeriksa apakah Call Stack kosong.
  2. Jika Call Stack kosong, ia akan memeriksa Microtask Queue. Jika ada tugas di Microtask Queue, ia akan memindahkan semua tugas tersebut satu per satu ke Call Stack untuk dieksekusi, sampai Microtask Queue kosong.
  3. Setelah Microtask Queue kosong, ia akan memeriksa Callback Queue. Jika ada tugas di Callback Queue, ia akan memindahkan tugas pertama dari antrean tersebut ke Call Stack untuk dieksekusi.
  4. Proses ini berulang terus-menerus.

4. Bagaimana JavaScript Event Loop Bekerja: Langkah Demi Langkah

Mari kita gunakan analogi untuk menjelaskan alur kerja Event Loop.

💡 Analogi Dapur Koki: Bayangkan Anda adalah seorang Koki (Call Stack) yang sedang sibuk memasak (menjalankan kode sinkron). Ada seorang Pelayan (Event Loop) yang tugasnya mengawasi Anda dan juga Meja Pesanan (Callback Queue / Microtask Queue). Lemari Es / Gudang Bahan (Web APIs) adalah tempat Anda bisa meletakkan pesanan untuk diproses di luar dapur utama, seperti membiarkan roti mengembang atau daging direndam.

  1. Koki Sibuk Memasak (Call Stack Eksekusi Kode Sinkron):

    • Koki mengambil resep (kode JS) dan mulai memasak hidangan utama (menjalankan fungsi sinkron). Setiap langkah resep (fungsi) masuk ke daftar tugas Koki (Call Stack).
  2. Mendelegasikan Tugas ke Gudang Bahan (Web APIs):

    • Jika resep membutuhkan sesuatu yang memakan waktu lama (misalnya, “panggang kue selama 30 menit” atau “ambil bahan dari gudang”), Koki tidak akan menunggu. Ia akan menuliskan catatan “panggang kue” dan “ambil bahan” lalu menyerahkannya ke bagian Gudang Bahan (Web APIs).
    • Koki (Call Stack) kemudian bebas melanjutkan memasak hidangan lain.
  3. Gudang Bahan Selesai, Letakkan di Meja Pesanan (Queues):

    • Setelah 30 menit, kue matang. Bagian Gudang Bahan akan meletakkan kue di Meja Pesanan (Callback Queue).
    • Jika ada pesanan “sangat penting” yang harus disajikan segera setelah Koki sedikit senggang (misalnya, ‘cek apakah saus sudah kental’), itu akan diletakkan di Meja Pesanan Prioritas (Microtask Queue).
  4. Pelayan Mengawasi (Event Loop Memantau):

    • Pelayan (Event Loop) terus-menerus mengawasi Koki.
    • Jika Koki sedang sibuk (Call Stack tidak kosong), Pelayan tidak akan mengganggunya.
    • Jika Koki sudah tidak punya tugas di tangannya (Call Stack kosong):
      • Pelayan akan melihat ke Meja Pesanan Prioritas (Microtask Queue) terlebih dahulu. Jika ada kue di sana, ia akan membawanya satu per satu ke Koki untuk disajikan, sampai Meja Pesanan Prioritas kosong.
      • Baru setelah Meja Pesanan Prioritas kosong, Pelayan akan melihat ke Meja Pesanan Biasa (Callback Queue). Ia akan mengambil satu kue dari sana dan membawanya ke Koki untuk disajikan.
  5. Loop Berulang:

    • Proses ini terus berulang. Koki selalu memprioritaskan tugas yang langsung ada di tangannya, lalu tugas dari Meja Prioritas, lalu tugas dari Meja Biasa.

Contoh Kode Konkret:

console.log('Mulai'); // 1. Eksekusi sinkron

setTimeout(() => {
  console.log('Ini dari setTimeout'); // 4. Masuk Macrotask Queue, dieksekusi setelah Microtask Queue kosong
}, 0); // Waktu 0ms hanya berarti "secepatnya setelah Call Stack kosong"

Promise.resolve().then(() => {
  console.log('Ini dari Promise'); // 3. Masuk Microtask Queue, dieksekusi setelah Call Stack kosong
});

console.log('Selesai'); // 2. Eksekusi sinkron

Urutan Eksekusi:

  1. console.log('Mulai') -> Call Stack -> Output: Mulai
  2. setTimeout dipanggil -> callback-nya diserahkan ke Web APIs. Setelah 0ms, Web APIs memasukkan callback console.log('Ini dari setTimeout') ke Macrotask Queue.
  3. Promise.resolve().then() dipanggil -> callback console.log('Ini dari Promise') masuk ke Microtask Queue.
  4. console.log('Selesai') -> Call Stack -> Output: Selesai
  5. Call Stack kini kosong. Event Loop mulai bekerja.
  6. Event Loop melihat Microtask Queue. Ada callback console.log('Ini dari Promise'). Event Loop memindahkannya ke Call Stack.
  7. console.log('Ini dari Promise') dieksekusi -> Output: Ini dari Promise. Call Stack kosong lagi.
  8. Event Loop melihat Microtask Queue. Sudah kosong.
  9. Event Loop melihat Macrotask Queue. Ada callback console.log('Ini dari setTimeout'). Event Loop memindahkannya ke Call Stack.
  10. console.log('Ini dari setTimeout') dieksekusi -> Output: Ini dari setTimeout. Call Stack kosong lagi.
  11. Event Loop melihat kedua queue, keduanya kosong. Proses selesai.

Output Akhir:

Mulai
Selesai
Ini dari Promise
Ini dari setTimeout

Ini menunjukkan betapa pentingnya prioritas Microtask Queue!

5. Macrotasks vs. Microtasks: Siapa yang Lebih Dulu?

Memahami perbedaan dan prioritas antara macrotasks dan microtasks adalah kunci untuk memprediksi perilaku kode asynchronous Anda dan menghindari bug yang sulit ditemukan.

📌 Aturan Emas: Event Loop akan mengosongkan seluruh Microtask Queue setiap kali Call Stack kosong, sebelum mengambil satu tugas dari Macrotask Queue dan sebelum browser melakukan rendering ulang.

Macrotasks

Macrotasks adalah “tugas besar” yang biasanya melibatkan interaksi dengan lingkungan browser atau sistem I/O. Eksekusi macrotask tunggal diikuti oleh pengosongan Microtask Queue, dan kemudian potensi rendering browser, sebelum macrotask berikutnya diproses.

Contoh Macrotasks:

Microtasks

Microtasks adalah “tugas kecil” yang memiliki prioritas lebih tinggi. Mereka dieksekusi setelah Call Stack kosong dan sebelum browser merender ulang atau memproses macrotask berikutnya. Semua microtasks yang ada di antrean akan dieksekusi secara berurutan.

Contoh Microtasks:

Contoh Perbandingan Prioritas:

console.log('Start');

setTimeout(() => {
  console.log('Macrotask 1 (setTimeout)');
  Promise.resolve().then(() => {
    console.log('Microtask dalam Macrotask 1');
  });
}, 0);

Promise.resolve().then(() => {
  console.log('Microtask 1 (Promise)');
});

setTimeout(() => {
  console.log('Macrotask 2 (setTimeout)');
}, 0);

console.log('End');

Output yang Diharapkan:

Start
End
Microtask 1 (Promise)
Macrotask 1 (setTimeout)
Microtask dalam Macrotask 1
Macrotask 2 (setTimeout)

Penjelasan:

  1. Start dan End dieksekusi sinkron.
  2. Promise.resolve().then() masuk ke Microtask Queue.
  3. Kedua setTimeout() masuk ke Macrotask Queue.
  4. Call Stack kosong. Event Loop mengosongkan Microtask Queue, mengeksekusi Microtask 1 (Promise).
  5. Microtask Queue kosong. Event Loop mengambil satu macrotask (Macrotask 1 (setTimeout)) dari Macrotask Queue.
  6. Saat Macrotask 1 dieksekusi, ia menjadwalkan Promise baru (Microtask dalam Macrotask 1), yang segera masuk ke Microtask Queue.
  7. Macrotask 1 selesai. Call Stack kosong lagi. Event Loop kembali mengosongkan Microtask Queue yang baru terisi, mengeksekusi Microtask dalam Macrotask 1.
  8. Microtask Queue kosong. Event Loop mengambil satu macrotask berikutnya (Macrotask 2 (setTimeout)) dari Macrotask Queue.
  9. Macrotask 2 selesai. Call Stack kosong dan semua queue kosong.

6. Mengoptimalkan Responsivitas UI dengan Memahami Event Loop

Memahami Event Loop bukan hanya teori, tetapi kunci untuk menulis kode JavaScript yang berkinerja tinggi. Berikut adalah beberapa tips praktis:

⚠️ Hindari Main Thread Blocking

Ini adalah penyebab utama UI “membeku”. Jika Anda memiliki komputasi berat yang berjalan di thread utama (Call Stack), browser tidak akan bisa merespons input pengguna atau merender ulang UI.

// ❌ Blocking
function processLargeArrayBlocking(arr) {
  for (let i = 0; i < arr.length; i++) {
    // Lakukan komputasi berat
    console.log(`Processing item ${i}`);
  }
  console.log('Processing Selesai (Blocking)');
}

// ✅ Non-Blocking dengan setTimeout
function processLargeArrayNonBlocking(arr) {
  let i = 0;
  function processChunk() {
    const start = Date.now();
    while (i < arr.length && (Date.now() - start < 50)) { // Proses selama 50ms
      // Lakukan kom