Perintah node app.mjs terlihat selesai dalam sekejap, padahal di baliknya ada rangkaian tahapan yang rapi: proses Node dimulai, berkas dibaca dan dimuat, V8 mengeksekusi kode, lalu event loop mengambil alih pekerjaan asynchronous. Memahami rangkaian ini menjelaskan banyak perilaku yang sering mengejutkan pemula — misalnya mengapa callback setTimeout berjalan setelah seluruh kode sinkron selesai. Chapter ini mengikuti perjalanan satu perintah node app.mjs dari awal sampai proses keluar.

Perjalanan Satu Perintah #

Ketika kita menekan Enter pada perintah node app.mjs, empat tahap besar terjadi secara berurutan:

  1. Inisialisasi runtime — proses node menyiapkan V8, memulai libuv, dan memuat core modules ke memori. Pada tahap ini environment global seperti process, console, dan setTimeout siap dipakai.
  2. Pemuatan module — Node membaca berkas app.mjs, mengenali ekstensi .mjs sebagai penanda ES Modules, mem-parsing kodenya, lalu memuat setiap dependency yang di-import secara rekursif sampai seluruh pohon module siap.
  3. Eksekusi kode sinkron — V8 menjalankan baris teratas app.mjs sampai selesai. Semua kode yang bersifat sinkron — deklarasi variable, pemanggilan function, perulangan — dieksekusi berurutan tanpa jeda sampai baris terakhir.
  4. Event loop — operasi asynchronous yang ditemui selama eksekusi (timer, pembacaan berkas, koneksi jaringan) didaftarkan ke libuv. Setelah kode sinkron selesai, event loop mulai berputar: menjemput hasil operasi yang telah siap dan menjalankan callback-nya, berulang kali selama masih ada pekerjaan tertunda. Ketika antrean kosong, proses Node keluar.

Eksekusi Sinkron dan Antrean Asynchronous #

Kunci pemahaman Node.js ada pada urutan ini: seluruh kode sinkron berjalan lebih dulu sampai tuntas, baru operasi asynchronous dieksekusi hasilnya. Dua jenis antrean menampung pekerjaan asynchronous:

  • Microtask queue — menampung callback Promise. Antrean ini dikosongkan sepenuhnya setiap kali satu callback selesai dieksekusi, sebelum event loop melangkah ke pekerjaan berikutnya. Karena itu hasil Promise selalu mendahului callback setTimeout.
  • Macrotask (task) queue — menampung callback timer seperti setTimeout dan setInterval, serta hasil operasi I/O. Callback-nya dijemput event loop pada fase yang sesuai — callback setTimeout, misalnya, dijemput pada fase timers setelah waktu tunda tercapai.

setTimeout(..., 0) sering disalahartikan sebagai “jalankan segera”. Faktanya, penundaan nol hanya memindahkan callback ke antrean timers; callback tetap menunggu kode sinkron dan seluruh microtask selesai lebih dahulu.

Membuktikan Urutannya #

Program kecil berikut mencatat urutan eksekusi sesungguhnya antara kode sinkron, Promise, dan setTimeout:

js
console.log("1. Kode sinkron pertama dijalankan");

setTimeout(() => {
  console.log("4. Callback setTimeout masuk fase timers");
}, 0);

Promise.resolve().then(() => {
  console.log("3. Microtask Promise dieksekusi lebih dulu");
});

console.log("2. Kode sinkron terakhir selesai");

Output yang Dihasilkan:

text
1. Kode sinkron pertama dijalankan
2. Kode sinkron terakhir selesai
3. Microtask Promise dieksekusi lebih dulu
4. Callback setTimeout masuk fase timers

Mari kita telaah urutan yang tercetak:

  1. Baris 1: pernyataan console.log pertama dieksekusi langsung — kode sinkron selalu berjalan duluan.
  2. Baris 11: console.log terakhir tetap berjalan sebelum hasil Promise dan setTimeout, meskipun keduanya sudah “didaftarkan” lebih dulu. Pendaftaran hanya menaruh callback di antrean; callback belum dieksekusi.
  3. Baris 7-9 (output ke-3): Promise.resolve().then() membuat microtask. Begitu kode sinkron tuntas, microtask queue dikosongkan sehingga callback ini tercetak sebelum timer.
  4. Baris 3-5 (output ke-4): callback setTimeout dengan jeda 0 milidetik baru dijemput pada fase timers — paling akhir di antara ketiganya.

Event Loop dan Thread Pool #

Event loop diimplementasikan di pustaka libuv dan berputar melalui beberapa fase: timers (callback setTimeout/setInterval), poll (menunggu dan menjemput hasil I/O seperti data jaringan), serta check (callback setImmediate). Setiap putaran memproses satu batch pekerjaan, mengosongkan microtask queue, lalu berlanjut ke fase berikutnya. Bedah tuntas fase-fase ini tersedia pada panduan resmi event loop, timers, dan process.nextTick .

Satu detail yang sering terlewat: event loop berjalan di satu main thread, tetapi libuv juga mengelola thread pool berisi empat thread bawaan. Operasi tertentu — sebagian besar operasi berkas, pencarian DNS, dan sejumlah operasi node:crypto — dikerjakan di thread pool agar main thread tidak memblokir. Ukuran thread pool bisa diubah lewat environment variable UV_THREADPOOL_SIZE yang terdokumentasi di halaman CLI Node.js .

Kini dapur Node.js sudah tidak asing bagi kita: V8 mengeksekusi JavaScript, libuv menjadwalkan pekerjaan asynchronous, dan event loop menyatukan semuanya. Pertanyaan berikutnya wajar muncul — JavaScript juga berjalan di browser, jadi apa persisnya yang membedakan dua lingkungan tersebut? Chapter Node.js vs JavaScript di Browser membandingkan keduanya secara berdampingan.