Menu

Event Loop Node.js: Fase, nextTick dan setImmediate

Event loop Node.js menjalankan callback-mu dalam beberapa fase (timers, poll, check, close), dan di antara setiap callback ia mengosongkan antrean process.nextTick lalu antrean promise. Jalankan contohnya untuk melihat urutan pastinya.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Event loop Node.js adalah perulangan yang menentukan callback mana yang berjalan berikutnya setelah kode synchronous-mu selesai. Ia melewati beberapa fase dalam siklus yang tetap: timers (setTimeout, setInterval), pending callbacks, poll (I/O seperti pembacaan file dan data jaringan), check (setImmediate), dan close callbacks. Setelah setiap callback, Node lebih dulu mengosongkan antrean process.nextTick lalu antrean promise, jadi keduanya selalu berjalan sebelum loop lanjut.

Hasilnya sync, nextTick, promise, setTimeout, kebalikan dari urutan penulisannya. Kode synchronous selalu selesai lebih dulu, lalu Node mengosongkan antrean nextTick, lalu antrean promise, dan baru setelah itu loop dimulai dan sampai ke fase timers.

Fase-fase event loop

Loop milik Node berasal dari libuv, library C yang dipakai Node untuk I/O. Sebelum putaran pertama, ia menjalankan timer yang sudah jatuh tempo, lalu setiap putaran menjalankan fase-fase ini secara berurutan:

  1. Pending callbacks: beberapa callback sistem yang ditunda dari putaran sebelumnya, seperti sebagian error jaringan.
  2. Poll: event I/O baru dan callback-nya (pembacaan file selesai, data tiba di socket). Saat tidak ada hal lain yang dijadwalkan, loop menunggu di sini.
  3. Check: callback setImmediate.
  4. Close callbacks: event 'close', seperti socket yang dihancurkan.
  5. Timers: callback setTimeout dan setInterval yang waktunya sudah tiba.

Banyak panduan menaruh timers di urutan pertama setiap putaran, dan begitulah cara kerja Node sebelum versi 20 (libuv 1.45). Urutan callback tetap sama di kedua cara: timers dari satu putaran diikuti poll dari putaran berikutnya.

Ada dua antrean di luar fase-fase itu: antrean nextTick dan antrean microtask (callback promise dan queueMicrotask). Node mengosongkan keduanya setelah setiap callback, di fase mana pun, nextTick lebih dulu. Loop terus berputar selama masih ada yang ditunggu (timer, file atau socket yang terbuka, server) dan proses berhenti saat tidak ada lagi yang tersisa.

Ini adalah loop milik Node sendiri. Browser berbagi ide task dan microtask, tetapi tidak punya fase, tidak punya setImmediate, dan tidak punya process.nextTick; halaman event loop JavaScript membahas gambaran di tingkat bahasa.

process.nextTick vs promise

Keduanya berjalan sebelum loop lanjut, tetapi tidak dicampur. Node menjalankan seluruh antrean nextTick, termasuk tick yang ditambahkan selama ia berjalan, baru kemudian antrean promise. Tebak hasilnya:

Urutannya tick 1, tick 2, tick from tick, promise 1, promise 2. Tick yang dijadwalkan di dalam tick masuk ke antrean yang sama dan tetap berjalan sebelum promise mana pun, walaupun promise-nya dibuat lebih dulu.

Di tingkat atas sebuah ES module (file yang memakai import) keduanya bertukar: kode module itu sendiri berjalan di dalam sebuah promise job, jadi callback promise-nya selesai sebelum Node sampai ke antrean nextTick. Di dalam callback, aturan nextTick-lebih-dulu berlaku di kedua sistem module.

setImmediate vs setTimeout(fn, 0)

setTimeout(fn, 0) sebenarnya berarti "setelah paling sedikit 1 ms, di fase timers". setImmediate(fn) berarti "di fase check berikutnya". Mana yang berjalan lebih dulu tergantung di mana kamu memanggilnya.

Di dalam callback I/O urutannya pasti. Callback itu berjalan di fase poll, fase check datang tepat setelahnya, dan fase timers baru datang sesudah itu:

Hasilnya nextTick, setImmediate, setTimeout, setiap kali. Itulah sebabnya setImmediate adalah pilihan yang tepat untuk "jalankan ini setelah pekerjaan I/O yang sekarang".

Di module utama, di luar callback mana pun, urutan keduanya tidak pasti:

// Order depends on how fast the process started
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));

Kalau sudah lewat paling sedikit 1 ms saat loop dimulai, timer sudah jatuh tempo dan tercetak lebih dulu; kalau belum, setImmediate yang menang. Jangan menulis kode yang bergantung pada salah satu hasil.

BerjalanDipakai untuk
process.nextTick(fn)Tepat setelah operasi yang sekarang, sebelum promiseMemancarkan event atau error setelah pemanggil memasang listener
Promise.then, queueMicrotaskSetelah antrean nextTick kosongKode async biasa
setImmediate(fn)Fase check, setelah callback I/OMemberi giliran ke loop di antara potongan pekerjaan
setTimeout(fn, 0)Fase timers, paling sedikit 1 ms kemudianDelay, saat momen tepatnya tidak penting

Memblokir event loop

Karena hanya ada satu thread JavaScript, callback yang berjalan lama menghentikan semua yang lain: timer, callback I/O, dan di server, setiap request lain. Delay sebuah timer adalah batas minimum, bukan jaminan.

loop finished tercetak lebih dulu, lalu true: timer sudah jatuh tempo setelah 10 ms tetapi tidak bisa berjalan sampai busy loop mengembalikan thread. Di server, loop yang sama di dalam route handler membekukan setiap request lain selama setengah detik.

Solusi untuk pekerjaan panjang adalah memecahnya dan memberi loop satu giliran di antara potongan-potongannya dengan setImmediate:

Timer mendapat giliran setelah part 1. Ganti setImmediate(() => work(part + 1)) dengan work(part + 1) lalu jalankan lagi: ketiga part berjalan sekaligus dan timer tercetak paling akhir.

Untuk pekerjaan yang berat dari awal sampai akhir, seperti pengolahan gambar, memecahnya tidak cukup: pindahkan ke luar main thread dengan worker_threads.

Kesalahan umum

Memanggil process.nextTick secara rekursif. Callback nextTick yang terus menjadwalkan nextTick lain tidak pernah membiarkan loop lanjut, jadi timer dan I/O kelaparan. Pakai setImmediate untuk pekerjaan yang berulang.

Memakai API sync di server. fs.readFileSync, crypto.pbkdf2Sync, dan panggilan JSON.parse yang besar memblokir loop untuk setiap user. Pakai versi async-nya di dalam request handler.

Mengira setTimeout(fn, 0) langsung berjalan. Ia berjalan setelah semua kode synchronous, setiap nextTick, dan setiap callback promise, dan baru setelah loop sampai ke fase timers.

Pertanyaan yang sering diajukan

Apakah Node.js single-threaded?

Kode JavaScript-mu berjalan di satu thread, jadi dua callback tidak pernah berjalan bersamaan. Node sendiri memakai lebih banyak thread: libuv menyimpan sebuah pool (empat thread secara default) untuk pekerjaan file system, lookup DNS, dan sebagian crypto, lalu menyerahkan hasilnya kembali ke loop.

Kapan program Node.js berhenti?

Saat event loop tidak punya apa-apa lagi untuk ditunggu: tidak ada timer, tidak ada I/O yang tertunda, tidak ada server atau socket yang terbuka. setInterval yang masih berjalan atau server yang sedang listen membuat proses tetap hidup sampai kamu menghentikan atau menutupnya.

Apakah event loop Node.js sama dengan milik browser?

Idenya sama, satu thread yang menjalankan callback yang mengantre, tetapi detailnya berbeda. Browser tidak punya setImmediate atau process.nextTick dan menyisipkan rendering di antara task, sedangkan Node menjalankan fase-fase milik libuv. Halaman event loop JavaScript membahas bagian yang sama di tingkat bahasa.

Bagaimana cara tahu event loop sedang terblokir?

Ukur seberapa terlambat ia berjalan: monitorEventLoopDelay() dari perf_hooks mencatat delay-nya, dan rata-rata di puluhan milidetik berarti ada callback yang menahan thread terlalu lama. Jalankan Node dengan --cpu-prof untuk melihat fungsi mana pelakunya.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai