Worker thread adalah thread tambahan di dalam proses Node yang menjalankan JavaScript-nya sendiri, supaya kerja hitung yang berat gak menghentikan server. Bayangin warung dengan satu kasir. Selama kasir menghitung stok gudang lima menit, antrean pembeli berhenti total. Solusinya: suruh pegawai di gudang belakang yang menghitung. Kasir tetap melayani pembeli, dan pegawai kembali membawa hasilnya. Kasir itu main thread (tempat event loop), pegawai gudang itu worker, dan "membawa hasilnya" itu postMessage.
🧪 Tebakan cepat: route
GET /statistikmenghitung bilangan prima sampai 3 juta di main thread, kira-kira 0,45 detik. Selama itu, Siti memintaGET /catatan, yang biasanya selesai dalam 1 milidetik. Berapa lama Siti menunggu? Tebak dulu, jawabannya ada di bagian "Masalahnya: satu thread untuk semua request".
Masalahnya: satu thread untuk semua request
Di pelajaran event loop kamu sudah lihat: JavaScript di Node jalan di satu thread. Membaca file atau query database gak masalah, karena Node menyerahkan tunggunya ke sistem dan lanjut melayani yang lain. Tapi kerja hitung (CPU-bound) dikerjakan thread itu sendiri, dan selama dia menghitung, gak ada yang lain yang jalan.
function hitungPrima(sampai) {
let jumlah = 0;
for (let n = 2; n <= sampai; n++) {
let prima = true;
for (let d = 2; d * d <= n; d++) {
if (n % d === 0) { prima = false; break; }
}
if (prima) jumlah++;
}
return jumlah;
}
app.get("/statistik", (req, res) => {
res.json({ prima: hitungPrima(3_000_000) }); // {"prima":216816}
});
Diukur dengan curl di laptop yang sama:
| Request Siti | Waktu |
|---|---|
GET /catatan, server santai |
0,001 detik |
GET /catatan, saat /statistik sedang menghitung |
0,38 detik |
Siti menunggu sampai hitungannya selesai, sekitar 0,4 detik, padahal route-nya sendiri cuma butuh 1 milidetik. Itu jawaban tebakannya. Sepuluh orang membuka /statistik berarti semua orang menunggu 4 detik.
worker_threads: pegawai di gudang belakang
Modul bawaan worker_threads menjalankan file JavaScript di thread lain. Kerja beratnya pindah ke sana:
// hitung-prima.js (jalan di worker)
const { parentPort, workerData } = require("node:worker_threads");
function hitungPrima(sampai) { /* sama seperti di atas */ }
parentPort.postMessage(hitungPrima(workerData.sampai));
// app.js (main thread)
const path = require("node:path");
const { Worker } = require("node:worker_threads");
app.get("/statistik", (req, res) => {
const worker = new Worker(path.join(__dirname, "hitung-prima.js"), {
workerData: { sampai: 3_000_000 },
});
worker.on("message", (jumlah) => res.json({ prima: jumlah }));
worker.on("error", (err) => res.status(500).json({ error: err.message }));
});
new Worker(file, { workerData })menyalakan thread baru yang menjalankanhitung-prima.js.workerDataadalah data awal yang dibawanya.- Di worker,
parentPort.postMessage(hasil)mengirim hasilnya ke main thread. - Di main thread,
worker.on("message", ...)menerimanya, lalu menjawab request. worker.on("error", ...)menangkap error yang dilempar di worker, supaya request-nya tetap dijawab dan server gak ikut mati.
Hasilnya tetap {"prima":216816} setelah sekitar 0,45 detik, tapi GET /catatan Siti sekarang selesai dalam 0,001 detik, karena main thread bebas.
Komunikasi dua arah
| Arah | Pengirim menulis | Penerima menulis |
|---|---|---|
| main ke worker, saat mulai | new Worker(file, { workerData }) |
workerData |
| main ke worker, kapan pun | worker.postMessage(data) |
parentPort.on("message", ...) |
| worker ke main | parentPort.postMessage(data) |
worker.on("message", ...) |
Data yang dikirim disalin, bukan dipakai bersama: worker punya memori sendiri. Angka, teks, array, dan object biasa bisa disalin. Function gak bisa: worker.postMessage({ hitung: () => 1 }) melempar DataCloneError: () => 1 could not be cloned. (Untuk memori yang benar-benar dipakai bersama ada SharedArrayBuffer, tapi itu jarang dibutuhkan.)
Di browser: Web Worker
Lab di halaman ini berjalan di browser, jadi yang dipakainya Web Worker, saudara worker_threads milik browser. Idenya sama persis: browser juga punya satu main thread, dan loop berat di sana membekukan halaman (animasi berhenti, klik gak direspons). Namanya saja yang beda:
Node worker_threads |
Browser Web Worker | |
|---|---|---|
| Membuat | new Worker("hitung-prima.js", { workerData }) |
new Worker(url) |
| Kirim dari worker | parentPort.postMessage(x) |
postMessage(x) |
| Terima di worker | parentPort.on("message", fn) |
self.onmessage = fn |
| Terima hasil | worker.on("message", fn) |
worker.onmessage = (e) => e.data |
Kapan perlu, kapan gak
- Perlu untuk kerja hitung: bilangan prima, resize gambar, membuat PDF, mengurai CSV besar, kompresi.
- Gak perlu untuk I/O: query database,
fetch, membaca file. Semua itu sudah gak memblokir, dan memindahkannya ke worker cuma menambah ongkos. - Menyalakan worker gak gratis: sekitar 30 milidetik dan beberapa MB memori untuk tiap worker. Untuk banyak request, pakai pool (sekumpulan worker yang dipakai ulang, misalnya library
piscina), jangan satu worker baru per request. worker_threads= thread di dalam satu proses.child_process= menjalankan program lain.cluster(pelajaran berikutnya) = beberapa proses Node yang berbagi satu port.
Di latihan: membagi kerja ke 4 bagian
Simulator latihan gak punya worker_threads, jadi yang kamu latih adalah cara membagi pekerjaan: menjumlahkan 1 sampai 1.000 dibagi ke 4 "worker", masing-masing 250 angka, lalu hasilnya digabung. computeSum(start, end) menjumlahkan dari start sampai end, ujungnya ikut dihitung. Di sini keempatnya masih jalan bergantian di satu thread. Dengan worker_threads, keempatnya bisa jalan bersamaan.
Worker 1: sum(1..250) = 31375
Worker 2: sum(251..500) = 93875
Worker 3: sum(501..750) = 156375
Worker 4: sum(751..1000) = 218875
Total result: 500500
Verifikasi (n*(n+1)/2): 500500
⚠️ Jebakan yang sering ketemu
- Loop berat di route. Semua request lain ikut menunggu. Pindahkan ke worker, atau pecah pekerjaannya.
- Worker untuk I/O. Query database dan
fetchgak memblokir, jadi worker cuma menambah ongkos tanpa hasil. - Worker baru per request. Sekitar 30 ms dan beberapa MB tiap kali. Pakai pool.
- Lupa
worker.on("error"). Error yang dilempar di worker sampai ke main thread sebagai eventerror. Tanpa pendengarnya, error itu gak tertangkap dan mematikan seluruh server.
TL;DR: JavaScript di Node jalan di satu thread, jadi kerja hitung yang berat (CPU-bound) memblokir event loop: GET /catatan yang biasanya 1 ms ikut menunggu 0,4 detik. Modul worker_threads memindahkannya ke thread lain: new Worker(file, { workerData }) di main thread, parentPort.postMessage(hasil) di worker, lalu worker.on("message") dan worker.on("error") di main thread. Data disalin, function gak bisa dikirim. Di browser, Web Worker melakukan hal yang sama dengan postMessage dan onmessage. Worker untuk kerja hitung, bukan untuk I/O, dan pakai pool kalau request-nya banyak.