Seluruh JavaScript kita sejauh ini berjalan di satu main thread: satu call stack, satu event loop, satu antrean microtask. Model ini cukup untuk pekerjaan I/O, tetapi menjadi penghambat ketika komputasi murni memakan waktu lama — selama perhitungan berjalan, event loop berhenti berputar dan seluruh aplikasi terasa tidak merespons. Module worker_threads adalah jawaban resmi Node.js untuk masalah ini: menjalankan kode JavaScript pada thread sistem operasi yang benar-benar terpisah. Chapter ini membangun worker pertama kita dari nol, menghubungkan komunikasi dua arah antar thread, lalu menutupnya dengan rapi.

Kapan Kita Membutuhkan Worker #

Kunci keputusannya adalah membedakan dua jenis beban kerja:

Jenis bebanContohPenanganan tepat
I/O-bound — menungguMembaca berkas, query basis data, request HTTPCukup pola asynchronous biasa; event loop tetap bebas melayani tugas lain selama menunggu
CPU-bound — menghitungKompresi data, parsing berkas besar, enkripsi berat, renderingPindahkan ke worker thread agar main thread tetap responsif

Ingat bahwa thread pool milik libuv tidak menolong di sini: thread tersebut mengerjakan operasi sistem tertentu (sebagian besar operasi berkas dan DNS), bukan kode JavaScript kita. Perhitungan fibonacci rekursif, misalnya, tetap berjalan di main thread berapa pun besar thread pool-nya. Hanya worker_threads yang memindahkan eksekusi JavaScript itu sendiri ke thread lain.

Worker Pertama: Komunikasi Dua Arah #

Sebuah worker dijalankan dari berkas terpisah. Thread utama membuat instance Worker yang menunjuk ke berkas tersebut, lalu kedua pihak bertukar pesan lewat method postMessage() dan event message. Berkas pertama adalah sang pekerja yang menerima tugas dan mengirim hasil:

js
import { parentPort } from "node:worker_threads";

function fib(n) {
  return n < 2 ? n : fib(n - 1) + fib(n - 2);
}

parentPort.on("message", (tugas) => {
  const hasil = fib(tugas.n);
  parentPort.postMessage({ n: tugas.n, hasil });
  parentPort.close();
});

Thread utama menjalankan worker, mengirim satu tugas, lalu mendengarkan balasan dan kabar keluar:

js
import { Worker } from "node:worker_threads";

const pekerja = new Worker(new URL("./pekerja.mjs", import.meta.url));

pekerja.on("message", (pesan) => {
  console.log(`Hasil dari worker: fib(${pesan.n}) = ${pesan.hasil}`);
});

pekerja.on("exit", (kode) => {
  console.log(`worker keluar dengan kode ${kode}`);
});

console.log("mengirim tugas fib(35) ke worker...");
pekerja.postMessage({ n: 35 });

Output yang Dihasilkan:

text
mengirim tugas fib(35) ke worker...
Hasil dari worker: fib(35) = 9227465
worker keluar dengan kode 0

Mari kita bedah dua berkas tersebut:

  1. pekerja.mjs baris 1: di dalam worker, object parentPort adalah kanal komunikasi satu-satunya ke thread utama — menggantikan process.stdout dan interaksi langsung lainnya.
  2. pekerja.mjs baris 6-10: setiap pesan masuk diproses; hasil perhitungan dikirim balik dengan postMessage(). Nilai fib(35) = 9.227.465 ini butuh komputasi rekursif puluhan juta pemanggilan — pekerjaan yang akan membekukan main thread bila dijalankan di sana.
  3. pekerja.mjs baris 9: parentPort.close() menutup kanal komunikasi. Tanpa baris ini, kanal yang terbuka membuat event loop worker tidak pernah kosong — worker tetap hidup dan proses utama ikut menggantung menunggunya. Setelah kanal ditutup, worker selesai secara alami.
  4. utama.mjs baris 3: new Worker(namaBerkas) memuat dan menjalankan berkas worker pada thread baru. Pola new URL(...) dengan import.meta.url memastikan lokasi berkas benar dari direktori mana pun proses dijalankan.
  5. utama.mjs baris 5-10: event message menerima hasil dari worker, sementara event exit memberi kode keluar — angka 0 menandakan worker selesai dengan bersih.

Menghentikan Worker dari Luar #

Worker pada contoh pertama menutup dirinya sendiri setelah tugasnya selesai. Untuk worker yang bekerja lama atau bahkan tanpa henti, thread utama bisa menghentikannya kapan saja lewat terminate() . Kali ini pekerjanya sengaja menghitung tanpa akhir dan tidak pernah membalas pesan:

js
// Worker ini menghitung fib(30) berulang tanpa henti,
// menyibukkan CPU tanpa pernah membalas pesan.
import { parentPort } from "node:worker_threads";

function fib(n) {
  return n < 2 ? n : fib(n - 1) + fib(n - 2);
}

while (true) {
  fib(30);
}

Sementara itu, thread utama cukup menjalankan worker lalu menghentikannya dari luar:

js
import { Worker } from "node:worker_threads";

const pekerja = new Worker(new URL("./pekerja-sibuk.mjs", import.meta.url));

console.log("worker sibuk menghitung, thread utama tetap responsif");

setTimeout(async () => {
  const kode = await pekerja.terminate();
  console.log(`worker dihentikan dari luar, kode keluar ${kode}`);
}, 50);

Output yang Dihasilkan:

text
worker sibuk menghitung, thread utama tetap responsif
worker dihentikan dari luar, kode keluar 1

Thread utama memulai worker lalu melanjutkan hidupnya sendiri — pesan pertama tercetak seketika, dan setTimeout 50 milidetik tetap berjalan tepat waktu meski CPU sibuk oleh perhitungan di thread lain (mesin kita mengalokasikan thread ke core yang berbeda). Method terminate() menghentikan eksekusi worker di titik mana pun berada, mengembalikan Promise yang berisi kode keluar 1 — penanda bahwa worker berakhir karena dihentikan, bukan karena selesai.

Berbagi Data Antar Thread #

postMessage() mengirim salinan data, bukan referensi — nilai disalin memakai algoritma structured clone, sehingga object yang dimodifikasi di satu thread tidak berubah di thread lain. Perilaku salinan ini menegaskan prinsip desain worker: thread tidak berbagi memori JavaScript, dan satu-satunya jalur data adalah pesan. Untuk data awal yang dibutuhkan sejak worker dibuat, opsi workerData pada new Worker(...) mengirim nilai tanpa menunggu postMessage(), dan dibaca di sisi worker melalui properti workerData.

Dua kemampuan lanjutan tersedia bila kebutuhan performa benar-benar menuntut: transfer list memindahkan kepemilikan ArrayBuffer antar thread tanpa menyalin, dan SharedArrayBuffer memungkinkan memori yang benar-benar dibagi — keduanya didokumentasikan di halaman worker_threads untuk kita pelajari saat kebutuhannya muncul.

Pesan antar thread menjadikan worker dapat diandalkan untuk komputasi, tetapi juga membuka pertanyaan baru: apa yang terjadi bila pekerjaan di worker atau callback asynchronous gagal? Error pada kode asynchronous menempuh jalur yang berbeda dari error sinkron — dan itulah topik penutup Section ini, Chapter Error pada Kode Asynchronous.