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:
- Inisialisasi runtime — proses
nodemenyiapkan V8, memulailibuv, dan memuat core modules ke memori. Pada tahap ini environment global sepertiprocess,console, dansetTimeoutsiap dipakai. - Pemuatan module — Node membaca berkas
app.mjs, mengenali ekstensi.mjssebagai penanda ES Modules, mem-parsing kodenya, lalu memuat setiap dependency yang di-importsecara rekursif sampai seluruh pohon module siap. - Eksekusi kode sinkron — V8 menjalankan baris teratas
app.mjssampai selesai. Semua kode yang bersifat sinkron — deklarasi variable, pemanggilan function, perulangan — dieksekusi berurutan tanpa jeda sampai baris terakhir. - 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 hasilPromiseselalu mendahului callbacksetTimeout. - Macrotask (task) queue — menampung callback timer seperti
setTimeoutdansetInterval, serta hasil operasi I/O. Callback-nya dijemput event loop pada fase yang sesuai — callbacksetTimeout, 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:
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:
1. Kode sinkron pertama dijalankan
2. Kode sinkron terakhir selesai
3. Microtask Promise dieksekusi lebih dulu
4. Callback setTimeout masuk fase timersMari kita telaah urutan yang tercetak:
- Baris 1: pernyataan
console.logpertama dieksekusi langsung — kode sinkron selalu berjalan duluan. - Baris 11:
console.logterakhir tetap berjalan sebelum hasilPromisedansetTimeout, meskipun keduanya sudah “didaftarkan” lebih dulu. Pendaftaran hanya menaruh callback di antrean; callback belum dieksekusi. - Baris 7-9 (output ke-3):
Promise.resolve().then()membuat microtask. Begitu kode sinkron tuntas, microtask queue dikosongkan sehingga callback ini tercetak sebelum timer. - Baris 3-5 (output ke-4): callback
setTimeoutdengan 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.