WEB-WORKERS MULTITHREADING WEB-PERFORMANCE FRONTEND-ARCHITECTURE JAVASCRIPT BROWSER PERFORMANCE-OPTIMIZATION DESIGN-PATTERNS CONCURRENCY RESPONSIVENESS DEVELOPER-EXPERIENCE

Membangun Worker Pool untuk Web Workers: Mengelola Multithreading Browser dengan Efisien dan Tangguh

⏱️ 13 menit baca
👨‍💻

Membangun Worker Pool untuk Web Workers: Mengelola Multithreading Browser dengan Efisien dan Tangguh

1. Pendahuluan

Pernahkah Anda mengalami aplikasi web yang “nge-freeze” atau terasa lambat saat melakukan tugas berat seperti memproses gambar, menghitung data besar, atau menjalankan algoritma kompleks? Ini adalah masalah umum yang disebabkan oleh main thread blocking. Di browser, JavaScript secara default berjalan di satu thread utama (main thread) yang juga bertanggung jawab untuk rendering UI dan menangani interaksi pengguna. Ketika thread ini sibuk dengan tugas komputasi intensif, UI akan berhenti merespons, menciptakan pengalaman pengguna yang buruk.

Untuk mengatasi ini, Web Workers hadir sebagai solusi. Web Workers memungkinkan Anda menjalankan skrip JavaScript di background, terpisah dari main thread, sehingga UI tetap responsif. Namun, bagaimana jika Anda memiliki banyak tugas berat yang perlu di-offload? Membuat satu worker untuk setiap tugas bisa jadi tidak efisien karena overhead inisialisasi worker. Di sinilah Worker Pool menjadi sangat berguna.

💡 Worker Pool adalah sebuah pola desain di mana Anda membuat dan mengelola sejumlah tetap Web Workers, siap untuk menerima dan memproses tugas. Bayangkan seperti sebuah “kolam” pekerja yang siaga. Ketika ada tugas baru, tugas tersebut akan diberikan kepada pekerja yang sedang menganggur di kolam tersebut. Jika semua pekerja sibuk, tugas akan masuk ke antrean dan menunggu hingga ada pekerja yang bebas. Pola ini menjaga efisiensi dengan menghindari pembuatan dan penghancuran worker secara terus-menerus.

Artikel ini akan memandu Anda membangun Worker Pool sederhana, memahami konsep di baliknya, dan menerapkan praktik terbaik untuk menjaga aplikasi web Anda tetap cepat dan responsif.

2. Kenapa Kita Butuh Worker Pool?

Mengapa tidak cukup hanya dengan membuat Web Worker baru setiap kali dibutuhkan? Ada beberapa alasan kuat untuk menggunakan Worker Pool:

📌 Contoh Kasus Nyata:

3. Konsep Dasar Worker Pool

Untuk membangun Worker Pool, kita perlu memahami beberapa komponen kunci:

Alur kerjanya mirip dengan antrean di bank atau kasir: Anda datang dengan tugas (misal: setor tunai), masuk antrean. Ketika ada teller (worker) yang bebas, Anda maju dan tugas Anda diproses. Setelah selesai, teller siap melayani pelanggan berikutnya.

4. Membangun Worker Pool Sederhana

Mari kita bangun Worker Pool sederhana menggunakan JavaScript. Kita akan memiliki tiga file:

  1. index.html: File HTML utama yang memuat aplikasi frontend.
  2. worker.js: Skrip yang akan dijalankan oleh setiap Web Worker (logika tugas berat).
  3. WorkerPool.js: Kelas JavaScript yang mengelola pool dan distribusi tugas.
  4. main.js: Skrip utama frontend yang menggunakan WorkerPool.

worker.js (Logika Tugas Berat)

File ini berisi kode yang akan dieksekusi oleh setiap worker. Untuk contoh ini, kita akan membuat worker yang menghitung faktorial dari sebuah angka.

// worker.js
// Setiap kali worker menerima pesan, ia akan menjalankan fungsi ini.
onmessage = (e) => {
    const { id, payload } = e.data; // id untuk identifikasi tugas, payload berisi data tugas
    console.log(`[Worker ${self.name}] Mulai memproses tugas ${id} dengan angka: ${payload.number}`);

    // ✅ Simulasi tugas berat: Menghitung faktorial
    // Ini akan memblokir worker thread, bukan main thread!
    let result = 1;
    for (let i = 2; i <= payload.number; i++) {
        result *= i;
    }

    // Mengirim hasil kembali ke main thread
    postMessage({ id, result, workerName: self.name });
    console.log(`[Worker ${self.name}] Selesai memproses tugas ${id}.`);
};

Dalam worker.js, self.name adalah nama yang kita berikan ke worker saat inisialisasi, berguna untuk debugging. onmessage adalah event handler yang dipanggil saat worker menerima pesan dari main thread. postMessage digunakan untuk mengirim hasil kembali.

WorkerPool.js (Manajemen Pool)

Ini adalah inti dari Worker Pool kita. Kelas ini akan mengelola pembuatan worker, antrean tugas, dan distribusi tugas.

// WorkerPool.js
class WorkerPool {
    constructor(workerScript, size) {
        this.workerScript = workerScript; // Path ke skrip worker
        this.size = size;                 // Ukuran pool (jumlah worker)
        this.workers = [];                // Array untuk menyimpan semua worker
        this.idleWorkers = [];            // Array untuk worker yang sedang menganggur
        this.taskQueue = [];              // Antrean tugas yang menunggu dieksekusi
        this.nextTaskId = 0;              // ID unik untuk setiap tugas

        this._initializeWorkers();
    }

    _initializeWorkers() {
        for (let i = 0; i < this.size; i++) {
            const worker = new Worker(this.workerScript, { name: `Worker-${i}` });
            worker.id = i; // ID unik untuk instance worker
            worker.isBusy = false; // Status awal: tidak sibuk

            // Event listener untuk pesan dari worker
            worker.onmessage = (e) => this._onWorkerMessage(worker, e);
            // Event listener untuk error dari worker
            worker.onerror = (e) => this._onWorkerError(worker, e);

            this.workers.push(worker);
            this.idleWorkers.push(worker); // Awalnya semua worker idle
        }
        console.log(`✅ Worker Pool diinisialisasi dengan ${this.size} worker.`);
    }

    _onWorkerMessage(worker, e) {
        const { id: taskId, result, workerName } = e.data;
        console.log(`[Worker Pool] Tugas ${taskId} selesai oleh ${workerName}.`);

        // Temukan dan selesaikan Promise yang terkait dengan tugas ini
        const task = this.taskQueue.find(t => t.id === taskId);
        if (task) {
            task.resolve(result); // Selesaikan Promise dengan hasil
            // Hapus tugas dari antrean setelah selesai
            this.taskQueue = this.taskQueue.filter(t => t.id !== taskId);
        }

        worker.isBusy = false;
        this.idleWorkers.push(worker); // Kembalikan worker ke pool idle
        this._scheduleNextTask(); // Coba jalankan tugas berikutnya jika ada
    }

    _onWorkerError(worker, e) {
        console.error(`⚠️ Error dari worker ${worker.name}:`, e);
        // Temukan dan reject Promise yang terkait dengan tugas yang gagal
        const task = this.taskQueue.find(t => t.workerId === worker.id);
        if (task) {
            task.reject(new Error(`Worker ${worker.name} gagal memproses tugas ${task.id}: ${e.message}`));
            this.taskQueue = this.taskQueue.filter(t => t.id !== task.id);
        }
        // ❌ Worker mungkin rusak. Pertimbangkan untuk me-restart worker atau log error lebih lanjut.
        // Untuk contoh sederhana ini, kita hanya menandai worker sebagai tidak sibuk dan mencoba melanjutkan.
        worker.isBusy = false;
        this._scheduleNextTask();
    }

    _scheduleNextTask() {
        // Jika ada tugas di antrean dan ada worker yang menganggur
        if (this.taskQueue.length > 0 && this.idleWorkers.length > 0) {
            const worker = this.idleWorkers.shift(); // Ambil worker idle pertama
            worker.isBusy = true;

            const nextTask = this.taskQueue[0]; // Ambil tugas pertama dari antrean (jangan dihapus dulu)
            nextTask.workerId = worker.id; // Simpan ID worker yang menangani tugas ini (untuk error handling)

            // Kirim tugas ke worker
            worker.postMessage({ id: nextTask.id, payload: nextTask.payload });
            console.log(`🎯 Menugaskan tugas ${nextTask.id} ke worker ${worker.name}`);
        }
    }

    runTask(payload) {
        return new Promise((resolve, reject) => {
            const taskId = this.nextTaskId++;
            // Tambahkan tugas ke antrean, beserta resolve/reject Promise-nya
            this.taskQueue.push({ id: taskId, payload, resolve, reject });
            this._scheduleNextTask(); // Coba jalankan tugas
        });
    }

    terminate() {
        this.workers.forEach(worker => worker.terminate());
        console.log('❌ Worker Pool dihentikan. Semua worker dimatikan.');
        this.workers = [];
        this.idleWorkers = [];
        this.taskQueue = []; // Hapus semua tugas yang belum selesai
    }
}

index.html dan main.js (Penggunaan Pool)

Sekarang, mari kita gunakan WorkerPool dari main thread.

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web Worker Pool Demo</title>
    <style>
        body { font-family: sans-serif; margin: 20px; }
        button { padding: 10px 15px; margin: 5px; cursor: pointer; }
        #results { margin-top: 20px; border: 1px solid #eee; padding: 10px; min-height: 50px; background: #f9f9f9; }
        .loading { color: gray; }
        .success { color: green; }
        .error { color: red; }
    </style>
</head>
<body>
    <h1>Web Worker Pool Demo</h1>
    <p>Ini adalah demo Worker Pool. Klik "Mulai Tugas Berat" untuk menjalankan banyak perhitungan faktorial di Web Workers tanpa memblokir UI.</p>
    <button id="startTasks">Mulai Tugas Berat (10x)</button>
    <button id="terminatePool">Hentikan Pool</button>
    <button id="resetPool">Reset Pool</button>
    <div id="results"></div>
    <script type="module" src="main.js"></script>
</body>
</html>
// main.js (di frontend utama)
import { WorkerPool } from './WorkerPool.js';

let pool; // Deklarasikan pool di luar agar bisa diakses oleh event listener

const resultsDiv = document.getElementById('results');
const startButton = document.getElementById('startTasks');
const terminateButton = document.getElementById('terminatePool');
const resetButton = document.getElementById('resetPool');

function initializePool() {
    if (pool) {
        pool.terminate(); // Pastikan pool sebelumnya dihentikan
    }
    pool = new WorkerPool('./worker.js', 4); // Inisialisasi pool dengan 4 worker
    console.log('Pool siap digunakan.');
    resultsDiv.innerHTML = '';
    startButton.disabled = false;
    terminateButton.disabled = false;
    resetButton.disabled = true; // Nonaktifkan reset sampai ada tugas
}

// Inisialisasi pool saat halaman dimuat
initializePool();

startButton.addEventListener('click', async () => {
    console.log('Memulai tugas-tugas...');
    resultsDiv.innerHTML = '<p class="loading">Memulai tugas berat di background...</p>';
    startButton.disabled = true; // Nonaktifkan tombol saat tugas berjalan
    resetButton.disabled = false;

    // Contoh banyak tugas yang akan di-offload
    const numbersToProcess = [10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95];
    
    try {
        // Buat array Promise, satu untuk setiap tugas yang dijalankan di pool
        const promises = numbersToProcess.map(num => pool.runTask({ number: num }));
        
        // Tunggu semua tugas selesai
        const allResults = await Promise.all(promises);
        
        resultsDiv.innerHTML = '<h3 class="success">Semua tugas selesai!</h3>';
        allResults.forEach((res, index) => {
            resultsDiv.innerHTML += `<p>Faktorial ${numbersToProcess[index]} = ${res}</p>`;
        });
        console.log('Semua tugas selesai:', allResults);
    } catch (error) {
        resultsDiv.innerHTML = `<h3 class="error">Ada tugas yang gagal!</h3>