1. Pengenalan Web Workers
JavaScript secara default berjalan di single thread — satu thread utama yang menangani semua hal: rendering UI, event handling, dan eksekusi kode. Jika ada proses berat (heavy computation), seluruh UI akan freeze (macet).
Web Workers memecahkan masalah ini dengan memungkinkan Anda menjalankan JavaScript di thread terpisah di background. Thread utama tetap responsif sementara worker melakukan komputasi berat.
TANPA Web Workers (Single Thread):
┌─────────────────────────────────────┐
│ Main Thread │
│ [Render UI] → [Heavy Task] → [Event] │
│ ↑ FREEZE! ↑ │
│ (UI tidak responsif) │
└─────────────────────────────────────┘
DENGAN Web Workers (Multi Thread):
┌────────────────────────┐ ┌──────────────┐
│ Main Thread │ │ Worker Thread │
│ [Render UI] → [Events] │◄──►│ [Heavy Task] │
│ (tetap responsif!) │ │ (di background)│
└────────────────────────┘ └──────────────┘
↕ postMessage() ↕ postMessage()
(komunikasi async)
Jenis-Jenis Web Workers
| Jenis | Deskripsi | Akses DOM? |
|---|---|---|
| Dedicated Worker | Dimiliki oleh satu tab/halaman saja | ❌ Tidak |
| Shared Worker | Bisa dibagi antar tab/halaman yang sama origin | ❌ Tidak |
| Service Worker | Proxy jaringan untuk caching & offline (topik terpisah) | ❌ Tidak |
Web Workers tidak bisa mengakses DOM, window, document, atau parent object. Mereka berjalan di konteks terpisah. Komunikasi dengan thread utama hanya bisa melalui postMessage() dan onmessage. Workers juga tidak bisa mengakses localStorage (tapi bisa IndexedDB).
2. Dedicated Worker
Dedicated Worker adalah jenis worker yang paling umum. Satu worker dimiliki oleh satu halaman web dan tidak bisa diakses dari tab lain.
Membuat Dedicated Worker
// ===== Membuat Dedicated Worker =====
// File worker dipisah: 'worker.js'
const worker = new Worker('worker.js');
// Kirim pesan ke worker
worker.postMessage({
command: 'hitung',
data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
});
// Terima hasil dari worker
worker.onmessage = function(event) {
console.log('Hasil dari worker:', event.data);
};
// Handle error
worker.onerror = function(error) {
console.error('Worker error:', error.message);
};
// Terminasi worker jika sudah tidak dibutuhkan
// worker.terminate();
// ===== worker.js =====
// File ini berjalan di thread terpisah!
// Terima pesan dari main thread
self.onmessage = function(event) {
const { command, data } = event.data;
if (command === 'hitung') {
// Proses berat di sini — tidak memblokir UI!
let total = 0;
for (let i = 0; i < data.length; i++) {
total += data[i];
}
// Kirim hasil kembali ke main thread
self.postMessage({
result: total,
processed: data.length
});
}
};
// 'self' merujuk ke worker global scope (mirip 'window' di main thread)
console.log('Worker siap menerima tugas!');
Worker dengan Structured Clone
// ===== Data yang bisa dikirim ke worker =====
// postMessage menggunakan "structured clone algorithm"
// Data di-CLONE, bukan di-reference!
const worker = new Worker('data-worker.js');
// ✅ Bisa dikirim:
worker.postMessage({ nama: 'Beebane', umur: 25 }); // Object
worker.postMessage([1, 2, 3, { nested: true }]); // Array
worker.postMessage('Hello World'); // String
worker.postMessage(42); // Number
worker.postMessage(new Date()); // Date
worker.postMessage(new Map([['key', 'value']])); // Map
worker.postMessage(new Set([1, 2, 3])); // Set
worker.postMessage(new Blob(['data'])); // Blob
worker.postMessage(new ArrayBuffer(8)); // ArrayBuffer
// ❌ TIDAK bisa dikirim:
// worker.postMessage(function() {}); // Function (non-cloneable)
// worker.postMessage(document); // DOM nodes
// worker.postMessage(window); // Window object
// worker.postMessage(new WeakMap()); // WeakMap/WeakSet
3. Komunikasi postMessage
Komunikasi antara main thread dan worker menggunakan postMessage() dan onmessage (atau addEventListener('message', ...)). Data dikirim secara asynchronous dan di-clone.
// ===== MessageChannel: Direct Communication =====
// Channel memberikan 2 port yang terhubung
const channel = new MessageChannel();
const port1 = channel.port1;
const port2 = channel.port2;
// Kirim worker dengan salah satu port
const worker = new Worker('channel-worker.js');
worker.postMessage('init', [channel.port2]);
// Gunakan port1 untuk komunikasi langsung
port1.onmessage = function(event) {
console.log('Dari worker via channel:', event.data);
};
port1.postMessage({ task: 'kalkulasi', value: 100 });
// ===== channel-worker.js =====
let port;
self.onmessage = function(event) {
if (event.data === 'init') {
// Terima port dari main thread
port = event.ports[0];
port.onmessage = function(e) {
const { task, value } = e.data;
if (task === 'kalkulasi') {
const hasil = Math.pow(value, 2);
port.postMessage({ task, result: hasil });
}
};
}
};
Pola Request-Response
// ===== WorkerPool: Manager untuk Banyak Worker =====
class WorkerPool {
constructor(workerScript, poolSize = navigator.hardwareConcurrency || 4) {
this.workers = [];
this.queue = [];
this.activeWorkers = 0;
for (let i = 0; i < poolSize; i++) {
const worker = new Worker(workerScript);
worker.busy = false;
this.workers.push(worker);
}
console.log(`Worker pool dibuat: ${poolSize} workers`);
}
exec(data) {
return new Promise((resolve, reject) => {
const task = { data, resolve, reject };
const idleWorker = this.workers.find(w => !w.busy);
if (idleWorker) {
this._runTask(idleWorker, task);
} else {
this.queue.push(task);
}
});
}
_runTask(worker, task) {
worker.busy = true;
this.activeWorkers++;
const handler = (event) => {
worker.removeEventListener('message', handler);
worker.removeEventListener('error', errorHandler);
worker.busy = false;
this.activeWorkers--;
task.resolve(event.data);
// Proses task berikutnya dari queue
if (this.queue.length > 0) {
const next = this.queue.shift();
this._runTask(worker, next);
}
};
const errorHandler = (error) => {
worker.removeEventListener('message', handler);
worker.removeEventListener('error', errorHandler);
worker.busy = false;
this.activeWorkers--;
task.reject(error);
};
worker.addEventListener('message', handler);
worker.addEventListener('error', errorHandler);
worker.postMessage(task.data);
}
terminate() {
this.workers.forEach(w => w.terminate());
this.workers = [];
}
}
// Penggunaan:
const pool = new WorkerPool('compute-worker.js', 4);
// Jalankan 10 tugas secara paralel
const tasks = Array.from({ length: 10 }, (_, i) =>
pool.exec({ type: 'faktorial', value: i + 1 })
);
Promise.all(tasks).then(results => {
console.log('Semua hasil:', results);
pool.terminate();
});
4. Shared Worker
Shared Worker bisa digunakan bersama oleh beberapa tab atau halaman dari origin yang sama. Ini berguna untuk berbagi state antar tab atau mengurangi resource.
// ===== shared-worker.js =====
let connections = 0;
let sharedState = { counter: 0, messages: [] };
self.onconnect = function(event) {
connections++;
const port = event.ports[0];
console.log(`Tab baru terkoneksi! Total: ${connections}`);
// Kirim state saat ini ke tab baru
port.postMessage({
type: 'init',
state: sharedState,
connections
});
port.onmessage = function(e) {
const { type, payload } = e.data;
switch (type) {
case 'increment':
sharedState.counter++;
broadcastToAll({ type: 'update', state: sharedState });
break;
case 'sendMessage':
sharedState.messages.push(payload);
broadcastToAll({ type: 'update', state: sharedState });
break;
case 'getState':
port.postMessage({ type: 'state', state: sharedState });
break;
}
};
port.start();
};
function broadcastToAll(message) {
// Broadcast ke semua port yang terhubung
// (perlu menyimpan port dalam array)
self.clients?.forEach(client => client.postMessage(message));
}
// ===== main.js — Menggunakan Shared Worker =====
// SharedWorker API mirip Dedicated Worker
const sharedWorker = new SharedWorker('shared-worker.js');
// Akses port dan mulai komunikasi
sharedWorker.port.start();
// Terima pesan dari shared worker
sharedWorker.port.onmessage = function(event) {
const { type, state, connections } = event.data;
if (type === 'init') {
console.log(`Terhubung! Total tab: ${connections}`);
updateUI(state);
}
if (type === 'update') {
console.log('State berubah:', state);
updateUI(state);
}
};
// Kirim perintah ke shared worker
function incrementCounter() {
sharedWorker.port.postMessage({ type: 'increment' });
}
function sendChatMessage(text) {
sharedWorker.port.postMessage({
type: 'sendMessage',
payload: { text, timestamp: Date.now() }
});
}
function updateUI(state) {
document.getElementById('counter').textContent = state.counter;
document.getElementById('messages').innerHTML =
state.messages.map(m => `<p>${m.text}</p>`).join('');
}
Gunakan Dedicated Worker untuk tugas yang spesifik untuk satu halaman (proses gambar, kalkulasi). Gunakan Shared Worker ketika beberapa tab perlu berbagi state atau koneksi (WebSocket, data sinkronisasi).
5. Transferable Objects
Secara default, postMessage() mengclone data. Untuk data besar (buffer gambar, data audio), cloning lambat. Transferable Objects memindahkan kepemilikan data tanpa cloning — jauh lebih cepat.
// ===== Transfer vs Clone =====
// CLONE (default) — data di-copy, lambat untuk data besar
worker.postMessage(bigArray); // Data di-clone → lambat
// TRANSFER — data dipindahkan, sangat cepat
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
worker.postMessage(buffer, [buffer]);
// buffer sekarang "neutered" (kosong) di sender
console.log(buffer.byteLength); // → 0 (sudah dipindahkan!)
// ===== Contoh: Proses Data Gambar =====
async function processImageInWorker(imageData) {
const worker = new Worker('image-worker.js');
return new Promise((resolve) => {
// Kirim data gambar dengan transfer (zero-copy!)
const buffer = imageData.data.buffer;
worker.postMessage(
{ type: 'process', data: buffer },
[buffer] // Transfer list — buffer dipindahkan!
);
worker.onmessage = (event) => {
const { processedBuffer, width, height } = event.data;
// Terima hasil dan buat ImageData baru
const result = new ImageData(
new Uint8ClampedArray(processedBuffer),
width,
height
);
worker.terminate();
resolve(result);
};
});
}
// ===== image-worker.js =====
self.onmessage = function(event) {
const { type, data } = event.data;
if (type === 'process') {
const pixels = new Uint8ClampedArray(data);
// Proses setiap pixel (contoh: grayscale)
for (let i = 0; i < pixels.length; i += 4) {
const avg = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3;
pixels[i] = avg; // R
pixels[i + 1] = avg; // G
pixels[i + 2] = avg; // B
// pixels[i + 3] = Alpha (tidak diubah)
}
// Kirim hasil dengan transfer
self.postMessage(
{
processedBuffer: data,
width: event.data.width || 0,
height: event.data.height || 0
},
[data] // Transfer kembali!
);
}
};
Tipe Transferable yang Didukung
| Tipe | Penggunaan |
|---|---|
| ArrayBuffer | Binary data — gambar, audio, video buffer |
| MessagePort | Port untuk MessageChannel |
| OffscreenCanvas | Canvas rendering di worker |
| ReadableStream | Stream data baca |
| WritableStream | Stream data tulis |
| TransformStream | Stream transform data |
| ImageBitmap | Decoded image data |
6. Inline Workers (Blob URL)
Tidak selalu perlu file terpisah untuk worker. Anda bisa membuat worker dari Blob URL — cocok untuk worker yang kecil atau dibuat dinamis.
// ===== Inline Worker dari String =====
const workerCode = `
self.onmessage = function(event) {
const { num } = event.data;
// Fungsi fibonacci berat
function fib(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
const result = fib(num);
self.postMessage({ num, result });
};
`;
// Buat Blob dari kode string
const blob = new Blob([workerCode], { type: 'application/javascript' });
const blobURL = URL.createObjectURL(blob);
// Buat worker dari Blob URL
const worker = new Worker(blobURL);
worker.postMessage({ num: 40 });
worker.onmessage = function(event) {
console.log(`fib(${event.data.num}) = ${event.data.result}`);
URL.revokeObjectURL(blobURL); // Bersihkan URL
worker.terminate();
};
// ===== Inline Worker dari Function =====
function createWorkerFromFunction(fn) {
const code = `(${fn.toString()})()`;
const blob = new Blob([code], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
URL.revokeObjectURL(url);
return worker;
}
const sortWorker = createWorkerFromFunction(function() {
self.onmessage = function(e) {
const data = e.data;
// Sort berat di worker
data.sort((a, b) => a - b);
self.postMessage(data);
};
});
sortWorker.postMessage([5, 3, 8, 1, 9, 2, 7, 4, 6]);
sortWorker.onmessage = (e) => console.log('Sorted:', e.data);
7. Error Handling
// ===== Menangkap Error dari Worker =====
const worker = new Worker('risky-worker.js');
// Method 1: onerror
worker.onerror = function(event) {
console.error('Worker error:');
console.error(' Message:', event.message);
console.error(' File:', event.filename);
console.error(' Line:', event.lineno);
console.error(' Column:', event.colno);
event.preventDefault(); // Cegah error propagate ke window
};
// Method 2: addEventListener
worker.addEventListener('error', (event) => {
console.error('Error di worker:', event.message);
});
// ===== Di dalam Worker: =====
self.onerror = function(message, source, lineno, colno, error) {
console.error(`Worker error at ${source}:${lineno} — ${message}`);
// Kirim info error ke main thread
self.postMessage({
type: 'error',
message: message,
stack: error?.stack
});
};
// Atau gunakan try/catch
self.onmessage = function(event) {
try {
const result = prosesData(event.data);
self.postMessage({ type: 'success', result });
} catch (error) {
self.postMessage({
type: 'error',
message: error.message,
stack: error.stack
});
}
};
8. Use Cases Nyata
Kalkulasi Matematika Berat
// ===== Cari Bilangan Prima — Proses di Worker =====
// prime-worker.js
self.onmessage = function(event) {
const { max } = event.data;
const primes = [];
function isPrime(n) {
if (n < 2) return false;
for (let i = 2; i <= Math.sqrt(n); i++) {
if (n % i === 0) return false;
}
return true;
}
for (let i = 2; i <= max; i++) {
if (isPrime(i)) {
primes.push(i);
}
// Progress update setiap 10000 iterasi
if (i % 10000 === 0) {
self.postMessage({
type: 'progress',
current: i,
max: max,
percent: Math.round((i / max) * 100)
});
}
}
self.postMessage({
type: 'complete',
primes: primes,
count: primes.length
});
};
// Main thread
const primeWorker = new Worker('prime-worker.js');
primeWorker.postMessage({ max: 1000000 });
primeWorker.onmessage = function(event) {
if (event.data.type === 'progress') {
updateProgressBar(event.data.percent);
}
if (event.data.type === 'complete') {
console.log(`Ditemukan ${event.data.count} bilangan prima`);
primeWorker.terminate();
}
};
function updateProgressBar(percent) {
document.getElementById('progress').style.width = percent + '%';
document.getElementById('progress-text').textContent = percent + '%';
}
Parsing JSON Besar
// ===== Parse JSON besar di worker agar UI tidak freeze =====
function parseLargeJSON(jsonString) {
return new Promise((resolve, reject) => {
const worker = createWorkerFromFunction(function() {
self.onmessage = function(e) {
try {
const data = JSON.parse(e.data);
self.postMessage({ success: true, data });
} catch (err) {
self.postMessage({ success: false, error: err.message });
}
};
});
worker.onmessage = function(e) {
worker.terminate();
if (e.data.success) {
resolve(e.data.data);
} else {
reject(new Error(e.data.error));
}
};
worker.onerror = function(e) {
worker.terminate();
reject(e);
};
worker.postMessage(jsonString);
});
}
// UI tetap responsif saat parse file besar!
const response = await fetch('/data/massive-dataset.json');
const text = await response.text();
const data = await parseLargeJSON(text);
console.log('Data diparse tanpa freeze!');
9. Best Practices
// ===== Wrapper yang Clean untuk Worker =====
class TaskWorker {
constructor(workerScript) {
this.worker = new Worker(workerScript);
this.taskId = 0;
this.pendingTasks = new Map();
this._setupListener();
}
_setupListener() {
this.worker.onmessage = (event) => {
const { taskId, result, error } = event.data;
const task = this.pendingTasks.get(taskId);
if (task) {
this.pendingTasks.delete(taskId);
if (error) {
task.reject(new Error(error));
} else {
task.resolve(result);
}
}
};
this.worker.onerror = (event) => {
event.preventDefault();
// Reject semua pending tasks
this.pendingTasks.forEach((task) => {
task.reject(new Error(event.message));
});
this.pendingTasks.clear();
};
}
run(data, transferables = []) {
return new Promise((resolve, reject) => {
const taskId = ++this.taskId;
this.pendingTasks.set(taskId, { resolve, reject });
this.worker.postMessage(
{ taskId, data },
transferables
);
});
}
terminate() {
this.worker.terminate();
this.pendingTasks.forEach(task =>
task.reject(new Error('Worker terminated'))
);
this.pendingTasks.clear();
}
}
// Penggunaan:
const worker = new TaskWorker('compute-worker.js');
try {
const result = await worker.run({ type: 'heavy', data: bigArray });
console.log('Selesai:', result);
} catch (err) {
console.error('Gagal:', err);
}
worker.terminate();
Checklist Best Practices
| Praktik | Penjelasan |
|---|---|
| Gunakan Transferable | Untuk data > 100KB, gunakan transfer daripada clone |
| Worker Pool | Jangan buat worker baru untuk setiap tugas — reuse! |
| Terminate | Selalu terminate worker jika sudah tidak dibutuhkan |
| Error Handling | Selalu tangkap error dari worker |
| Progress Report | Kirim progress update untuk tugas berat |
| Minimize Data | Kirim hanya data yang diperlukan, bukan object besar |
| Feature Detection | Cek typeof Worker sebelum membuat worker |
10. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut: