Web Development

Web Workers: Multithreading di JavaScript

Jalankan proses berat di background tanpa memblokir UI — dedicated workers, shared workers, transferable objects, dan paralel computing di browser

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.

Diagram: Single Thread vs Multi Thread
  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 WorkerDimiliki oleh satu tab/halaman saja❌ Tidak
Shared WorkerBisa dibagi antar tab/halaman yang sama origin❌ Tidak
Service WorkerProxy jaringan untuk caching & offline (topik terpisah)❌ Tidak
⚠️ Batasan Web Workers

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

JavaScript — main.js (Thread Utama)
// ===== 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();
JavaScript — worker.js (Thread Worker)
// ===== 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

JavaScript — 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.

JavaScript — MessageChannel
// ===== 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 });
JavaScript — channel-worker.js
// ===== 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

JavaScript — Request-Response Pattern
// ===== 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.

JavaScript — shared-worker.js
// ===== 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));
}
JavaScript — Menggunakan Shared Worker
// ===== 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('');
}
💡 Dedicated vs Shared Worker

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.

JavaScript — Transferable Objects
// ===== 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);
        };
    });
}
JavaScript — image-worker.js
// ===== 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
ArrayBufferBinary data — gambar, audio, video buffer
MessagePortPort untuk MessageChannel
OffscreenCanvasCanvas rendering di worker
ReadableStreamStream data baca
WritableStreamStream data tulis
TransformStreamStream transform data
ImageBitmapDecoded 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.

JavaScript — Inline Worker dari Blob
// ===== 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

JavaScript — Error Handling di Workers
// ===== 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

JavaScript — Prime Number Calculator
// ===== 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

JavaScript — JSON Parser Worker
// ===== 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

JavaScript — Worker Wrapper dengan Promise
// ===== 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

PraktikPenjelasan
Gunakan TransferableUntuk data > 100KB, gunakan transfer daripada clone
Worker PoolJangan buat worker baru untuk setiap tugas — reuse!
TerminateSelalu terminate worker jika sudah tidak dibutuhkan
Error HandlingSelalu tangkap error dari worker
Progress ReportKirim progress update untuk tugas berat
Minimize DataKirim hanya data yang diperlukan, bukan object besar
Feature DetectionCek typeof Worker sebelum membuat worker

10. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut:

Pertanyaan 1: Bisakah Web Worker mengakses DOM halaman?

a) Ya, bisa mengakses semua elemen DOM
b) Ya, tapi hanya bisa membaca, tidak bisa menulis
c) Tidak, worker tidak bisa mengakses DOM
d) Tergantung jenis browser

Pertanyaan 2: Apa perbedaan utama antara Dedicated Worker dan Shared Worker?

a) Dedicated Worker lebih cepat dari Shared Worker
b) Shared Worker bisa dibagi antar tab, Dedicated Worker tidak
c) Dedicated Worker bisa mengakses DOM
d) Tidak ada perbedaan

Pertanyaan 3: Apa fungsi dari "transferable objects" di postMessage?

a) Mengenkripsi data saat dikirim
b) Memindahkan data tanpa cloning untuk performa lebih cepat
c) Mengkompresi data sebelum dikirim
d) Mengubah tipe data otomatis

Pertanyaan 4: Apa yang terjadi pada ArrayBuffer setelah ditransfer ke worker?

a) Tetap bisa diakses di main thread
b) Menjadi "neutered" — byteLength menjadi 0
c) Dihapus secara otomatis
d) Menjadi read-only

Pertanyaan 5: Method apa yang digunakan untuk komunikasi dari main thread ke worker?

a) worker.send()
b) worker.emit()
c) worker.postMessage()
d) worker.dispatch()
🔍 Zoom
100%
🎨 Tema