Pengantar Event Loop JavaScript
JavaScript dikenal sebagai bahasa pemrograman single-threaded, artinya ia hanya dapat mengeksekusi satu tugas pada satu waktu. Namun, pada kenyataannya, aplikasi web modern seringkali melakukan banyak hal secara bersamaan: mengambil data dari API, memperbarui antarmuka pengguna, merespons interaksi pengguna, dan banyak lagi. Bagaimana JavaScript mengelola semua ini tanpa memblokir thread utamanya dan membuat aplikasi terasa lambat atau tidak responsif?
Jawabannya terletak pada mekanisme yang disebut Event Loop. Event Loop adalah model konkurensi yang memungkinkan JavaScript untuk melakukan pekerjaan non-blokir (asynchronous) meskipun berjalan dalam satu thread. Memahami Event Loop adalah kunci untuk menulis kode JavaScript yang efisien, responsif, dan bebas dari performance bottlenecks. Lebih spesifik lagi, pemahaman tentang Microtasks dan Macrotasks adalah fondasi untuk menguasai Event Loop.
Artikel ini akan menguak misteri di balik Microtasks dan Macrotasks, menjelaskan peran mereka dalam Event Loop, dan bagaimana pengetahuan ini dapat membantu Anda mengoptimalkan performa aplikasi JavaScript Anda.
Memahami Konsep Asynchronous JavaScript
Sebelum kita menyelam lebih dalam ke Event Loop, mari kita segarkan kembali pemahaman tentang asynchronous JavaScript. Dalam pemrograman sinkron, setiap baris kode dieksekusi secara berurutan. Jika ada operasi yang memakan waktu lama (misalnya, permintaan jaringan), seluruh aplikasi akan ‘membeku’ hingga operasi tersebut selesai.
Asynchronous JavaScript memungkinkan operasi yang memakan waktu lama untuk ‘dieksekusi di latar belakang’ dan memberikan kontrol kembali ke thread utama. Setelah operasi latar belakang selesai, ia akan memberi tahu JavaScript untuk mengeksekusi fungsi callback yang telah ditentukan. Mekanisme ini mencegah aplikasi dari pembekuan dan menjaga antarmuka pengguna tetap responsif.
Contoh umum operasi asynchronous meliputi:
- Permintaan HTTP (
fetch,XMLHttpRequest) - Timer (
setTimeout,setInterval) - Event DOM (klik, input, dll.)
- Operasi I/O (membaca file)
Event Loop: Jantungnya Konkurensi JavaScript
Event Loop adalah orkestrator di balik semua operasi asynchronous di JavaScript. Ia bekerja bersama dengan beberapa komponen kunci lainnya:
- Call Stack: Tempat di mana JavaScript melacak fungsi mana yang sedang dieksekusi. Ketika sebuah fungsi dipanggil, ia ditambahkan ke stack; ketika selesai, ia dikeluarkan dari stack.
- Web APIs (atau Node.js APIs): Lingkungan browser (atau Node.js) menyediakan fungsionalitas di luar inti JavaScript, seperti DOM,
setTimeout,fetch, dan lainnya. Ketika fungsi-fungsi ini dipanggil, mereka diserahkan ke Web APIs untuk ditangani. - Callback Queue (atau Task Queue/Macrotask Queue): Setelah Web API menyelesaikan tugasnya, fungsi callback yang terkait dengan tugas tersebut tidak langsung masuk ke Call Stack. Sebaliknya, ia ditempatkan di Callback Queue.
- Microtask Queue: Sebuah antrean terpisah yang memiliki prioritas lebih tinggi daripada Callback Queue. Ini akan kita bahas lebih lanjut.
Mekanisme Event Loop sederhana: ia terus-menerus memantau Call Stack dan Callback Queue. Jika Call Stack kosong, Event Loop akan mengambil fungsi pertama dari Callback Queue dan memindahkannya ke Call Stack untuk dieksekusi.
Macrotasks: Pekerjaan Besar dalam Event Loop
Macrotasks (atau kadang disebut Tasks) adalah unit pekerjaan diskrit yang dieksekusi oleh Event Loop. Setiap iterasi Event Loop akan memproses satu Macrotask dari Macrotask Queue (Callback Queue) dan setelah itu, Event Loop akan memeriksa Microtask Queue.
Contoh Umum Macrotasks
Beberapa contoh operasi yang menghasilkan Macrotasks meliputi:
setTimeout()setInterval()setImmediate()(khusus Node.js)- I/O operasi (seperti pembacaan file atau permintaan jaringan)
- Event DOM (misalnya,
click,load,scroll) requestAnimationFrame()(secara teknis, ini adalah bagian dari siklus rendering browser, tetapi sering dikelompokkan sebagai macrotask karena dieksekusi per frame)
Bagaimana Macrotasks Dieksekusi
Ketika sebuah Macrotask selesai dieksekusi, Event Loop akan mengosongkan seluruh Microtask Queue sebelum mengambil Macrotask berikutnya dari Macrotask Queue. Ini adalah poin penting yang membedakan Microtasks dari Macrotasks.
Mari lihat contoh:
console.log('Start'); // 1. Sinkronus
setTimeout(() => {
console.log('Macrotask 1 (setTimeout)'); // 4. Macrotask
}, 0);
setTimeout(() => {
console.log('Macrotask 2 (setTimeout)'); // 5. Macrotask
}, 0);
console.log('End'); // 2. Sinkronus
Output yang diharapkan:
Start
End
Macrotask 1 (setTimeout)
Macrotask 2 (setTimeout)
Penjelasannya: Kode sinkronus dieksekusi terlebih dahulu. Setelah itu, Event Loop mengambil Macrotask pertama (`setTimeout`) dari antrean, mengeksekusinya, kemudian mengambil Macrotask kedua, dan seterusnya.
Microtasks: Prioritas Tinggi dalam Event Loop
Microtasks adalah tugas-tugas kecil yang memiliki prioritas eksekusi lebih tinggi daripada Macrotasks. Mereka dieksekusi setelah Call Stack kosong, tetapi sebelum Event Loop mengambil Macrotask berikutnya.
Contoh Umum Microtasks
Beberapa contoh operasi yang menghasilkan Microtasks meliputi:
- Callback
.then(),.catch(), dan.finally()dari sebuahPromise await(karena secara internal menggunakan Promise)MutationObserver(untuk memantau perubahan DOM)queueMicrotask()(API baru untuk menjadwalkan microtask secara eksplisit)
Bagaimana Microtasks Dieksekusi
Setelah Call Stack kosong dan semua kode sinkronus selesai, Event Loop akan memeriksa Microtask Queue. Jika ada Microtasks, Event Loop akan mengeksekusi semua Microtasks di antrean tersebut hingga kosong, sebelum beralih ke Macrotask berikutnya.
Perhatikan contoh berikut:
console.log('Start'); // 1. Sinkronus
setTimeout(() => {
console.log('Macrotask (setTimeout)'); // 5. Macrotask
}, 0);
Promise.resolve().then(() => {
console.log('Microtask 1 (Promise)'); // 3. Microtask
});
Promise.resolve().then(() => {
console.log('Microtask 2 (Promise)'); // 4. Microtask
});
console.log('End'); // 2. Sinkronus
Output yang diharapkan:
Start
End
Microtask 1 (Promise)
Microtask 2 (Promise)
Macrotask (setTimeout)
Penjelasannya: Setelah ‘Start’ dan ‘End’ dieksekusi, Call Stack kosong. Event Loop kemudian mengosongkan seluruh Microtask Queue (mengeksekusi kedua callback Promise) sebelum beralih ke Macrotask Queue dan mengeksekusi callback `setTimeout`.
Perbedaan Kritis Antara Microtasks dan Macrotasks
Memahami urutan eksekusi adalah kunci:
- Macrotask Queue (Task Queue): Menampung tugas-tugas seperti timer, I/O, event DOM. Event Loop memproses satu Macrotask per iterasi.
- Microtask Queue: Menampung tugas-tugas seperti callback Promise dan
MutationObserver. Event Loop memproses semua Microtasks di antrean hingga kosong, setelah setiap Macrotask selesai dan sebelum Macrotask berikutnya diambil.
Urutan eksekusi dalam satu siklus Event Loop adalah:
- Eksekusi semua kode sinkronus di Call Stack.
- Ketika Call Stack kosong, proses semua Microtasks di Microtask Queue.
- Setelah Microtask Queue kosong, ambil satu Macrotask dari Macrotask Queue dan eksekusi.
- Ulangi dari langkah 2.
Ini berarti jika Anda menjadwalkan banyak Microtasks secara berurutan, mereka semua akan dieksekusi sebelum Macrotask berikutnya memiliki kesempatan untuk berjalan. Ini bisa menjadi pedang bermata dua.
Dampak Terhadap Performa dan Responsivitas UI
Pemahaman ini sangat penting untuk performa. Jika Anda memiliki Macrotask yang memakan waktu sangat lama, atau jika Anda menjadwalkan terlalu banyak Microtasks yang berat secara komputasi, Anda bisa memblokir Event Loop. Ini akan menyebabkan:
- Jank UI: Antarmuka pengguna akan ‘membeku’ atau menjadi tidak responsif karena browser tidak dapat melakukan rendering atau merespons input pengguna.
- Latensi: Event atau animasi akan tertunda karena Event Loop sibuk memproses tugas lain.
Misalnya, jika Anda memiliki loop yang memproses jutaan data dalam sebuah Microtask, browser tidak akan dapat merender frame berikutnya atau merespons klik pengguna sampai semua Microtask tersebut selesai. Ini karena Event Loop tidak akan beralih ke Macrotask (seperti rendering atau event handler) sampai Microtask Queue kosong.
Strategi Optimasi Kode dengan Pemahaman Event Loop
Dengan pemahaman tentang Microtasks dan Macrotasks, Anda dapat menerapkan strategi optimasi berikut:
- Hindari Pekerjaan Berat di Microtasks: Karena Microtasks memiliki prioritas tinggi dan dieksekusi secara berurutan hingga habis, hindari menempatkan operasi komputasi yang sangat berat di dalam callback Promise atau Microtasks lainnya. Ini dapat memblokir Event Loop dan menyebabkan UI tidak responsif.
- Pecah Tugas Berat menjadi Macrotasks Kecil: Jika Anda memiliki operasi yang sangat memakan waktu, pertimbangkan untuk memecahnya menjadi beberapa bagian kecil dan menjadwalkannya sebagai Macrotasks menggunakan
setTimeout(task, 0). Ini memungkinkan Event Loop untuk secara berkala memproses event UI atau rendering di antara bagian-bagian tugas berat Anda, menjaga responsivitas. - Gunakan
requestAnimationFrameuntuk Animasi: Untuk animasi yang halus, gunakanrequestAnimationFrame. Ini menjamin bahwa callback Anda akan dieksekusi tepat sebelum browser melakukan repaint, memastikan animasi yang optimal dan menghindari jank. - Pahami Kapan Menggunakan
queueMicrotask(): APIqueueMicrotask()memungkinkan Anda secara eksplisit menjadwalkan sebuah Microtask. Ini berguna ketika Anda ingin mengeksekusi kode segera setelah kode sinkronus saat ini selesai, tetapi sebelum Event Loop mengambil Macrotask berikutnya. Gunakan dengan bijak untuk menghindari pemblokiran. - Manfaatkan Web Workers untuk Komputasi Intensif: Untuk tugas-tugas yang benar-benar memblokir dan tidak dapat dipecah, pertimbangkan untuk menggunakan Web Workers. Web Workers menjalankan JavaScript di thread terpisah, sehingga tidak akan memblokir thread UI utama.
Studi Kasus: Mengapa setTimeout(..., 0) Tidak Selalu Instan?
Banyak developer pemula seringkali bingung mengapa setTimeout(callback, 0) tidak selalu mengeksekusi callback secara instan, bahkan dengan delay 0 milidetik. Jawabannya terletak pada Event Loop dan sifat Macrotasks.
Ketika Anda memanggil setTimeout(callback, 0), callback tersebut tidak langsung dieksekusi. Sebaliknya, ia ditempatkan di Macrotask Queue (Callback Queue). Callback ini hanya akan dieksekusi ketika:
- Call Stack kosong (semua kode sinkronus telah selesai).
- Microtask Queue kosong (semua Microtasks telah selesai).
- Event Loop mengambil Macrotask tersebut dari Macrotask Queue.
Selain itu, browser memiliki minimum delay untuk setTimeout (biasanya 4ms setelah HTML5 spesifikasi). Jadi, bahkan dengan 0, ada kemungkinan penundaan minimal. Yang lebih penting, jika ada banyak Macrotasks atau Microtasks lain yang menunggu, callback Anda harus menunggu gilirannya. Ini adalah salah satu alasan mengapa setTimeout(..., 0) sering digunakan sebagai cara untuk ‘menunda’ eksekusi ke iterasi Event Loop berikutnya, memberikan kesempatan bagi browser untuk merender atau memproses event lainnya.
Kesimpulan
Event Loop, Microtasks, dan Macrotasks adalah fondasi penting dalam arsitektur konkurensi JavaScript. Memahami bagaimana mereka berinteraksi adalah esensial bagi setiap developer yang ingin membangun aplikasi web yang berkinerja tinggi dan responsif. Ingatlah:
- Kode sinkronus dieksekusi pertama.
- Microtasks dieksekusi setelah kode sinkronus selesai, dan semua Microtasks diantrean akan selesai sebelum Event Loop mengambil Macrotask berikutnya.
- Macrotasks dieksekusi satu per satu per iterasi Event Loop, dan setiap Macrotask diikuti oleh pengosongan Microtask Queue.
Dengan menerapkan pengetahuan ini, Anda dapat menulis kode yang lebih efisien, menghindari hambatan performa, dan memberikan pengalaman pengguna yang lebih baik.
