Worker Threads — Node.js

Node.js berjalan di single thread dengan event loop. Untuk tugas CPU-intensive, gunakan Worker Threads agar tidak memblokir event loop. Masalah single thread…

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 /statistik menghitung bilangan prima sampai 3 juta di main thread, kira-kira 0,45 detik. Selama itu, Siti meminta GET /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 }));
});

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

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

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.

Yang akan kamu pelajari