Loadding..

Web Workers là gì?

Web Workers là gì?


1. Giới thiệu

1.1. Web Workers là gì?

Web Workers là một API JavaScript cho phép chạy các script trong background threads (luồng nền), tách biệt hoàn toàn với main thread của trình duyệt.

┌─────────────────────────────────────────────────────────┐
│                    MAIN THREAD                          │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐     │
│  │   DOM   │  │   UI    │  │  Event  │  │  User   │     │
│  │ Render  │  │ Updates │  │ Handlers│  │ Input   │     │
│  └─────────┘  └─────────┘  └─────────┘  └─────────┘     │
└───────────────────────┬─────────────────────────────────┘
                        │ postMessage / onmessage
┌───────────────────────▼─────────────────────────────────┐
│                   WEB WORKER THREAD                     │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐      │
│  │   Heavy     │  │    Data     │  │   Complex   │      │
│  │ Computation │  │  Processing │  │ Algorithms  │      │
│  └─────────────┘  └─────────────┘  └─────────────┘      │
└─────────────────────────────────────────────────────────┘

1.2. Tại sao cần Web Workers?

JavaScript mặc định là single-threaded, nghĩa là:

  • Chỉ chạy một task tại một thời điểm
  • Main thread xử lý cả UI rendering và logic nghiệp vụ
  • Các tác vụ nặng sẽ block UI → trải nghiệm người dùng kém

Web Workers giải quyết vấn đề này bằng cách:

  • ✅ Chạy code trong background thread riêng biệt
  • ✅ Không block main thread
  • ✅ UI luôn responsive ngay cả khi xử lý tác vụ nặng

2. Cách hoạt động

2.1. Mô hình Message-Passing

Web Workers giao tiếp với main thread thông qua hệ thống message passing:

// Main Thread
const worker = new Worker('worker.js');

// Gửi message đến worker
worker.postMessage({ type: 'PROCESS', data: largeDataset });

// Nhận message từ worker
worker.onmessage = (event) => {
  console.log('Kết quả:', event.data);
};

// Xử lý lỗi
worker.onerror = (error) => {
  console.error('Worker error:', error.message);
};
// worker.js (Worker Thread)
self.onmessage = (event) => {
  const { type, data } = event.data;
  
  if (type === 'PROCESS') {
    const result = processData(data); // Heavy computation
    self.postMessage(result);
  }
};

2.2. Context Isolation

Workers chạy trong global context riêng biệt:

  • Không có quyền truy cập windowdocumentDOM
  • Có self object thay vì window
  • Có thể sử dụng: fetchIndexedDBWebSocketsWebCrypto

3. Các loại Web Workers

3.1. Dedicated Workers

Đặc điểm:

  • Chỉ phục vụ một script duy nhất
  • Lifecycle gắn liền với trang tạo ra nó
  • Phổ biến nhất và dễ sử dụng nhất
// Tạo Dedicated Worker
const worker = new Worker('dedicated-worker.js');

3.2. Shared Workers

Đặc điểm:

  • Có thể được nhiều scripts/tabs/iframes truy cập
  • Phải cùng origin
  • Phù hợp cho việc chia sẻ state giữa các tabs
// Tạo Shared Worker
const sharedWorker = new SharedWorker('shared-worker.js');

// Giao tiếp qua port
sharedWorker.port.start();
sharedWorker.port.postMessage('Hello');
sharedWorker.port.onmessage = (e) => console.log(e.data);
// shared-worker.js
const connections = [];

self.onconnect = (e) => {
  const port = e.ports[0];
  connections.push(port);
  
  port.onmessage = (event) => {
    // Broadcast đến tất cả connections
    connections.forEach(p => p.postMessage(event.data));
  };
  
  port.start();
};

3.3. Service Workers

Đặc điểm:

  • Chạy như proxy giữa browser và network
  • Dùng cho PWA, offline capabilities, push notifications
  • Lifecycle độc lập với trang web
// Đăng ký Service Worker
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW registered'))
    .catch(err => console.log('SW registration failed'));
}

3.4. So sánh các loại Workers

Đặc điểmDedicatedSharedService
Phạm vi1 scriptNhiều scriptsToàn domain
LifecycleTheo trangTheo connectionsĐộc lập
Use caseHeavy computationShare stateOffline/Caching
Network intercept

4. Cú pháp và API

4.1. Tạo Worker

// Cách 1: External file
const worker = new Worker('worker.js');

// Cách 2: Inline với Blob (ES6+)
const workerCode = `
  self.onmessage = (e) => {
    const result = e.data * 2;
    self.postMessage(result);
  };
`;
const blob = new Blob([workerCode], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));

// Cách 3: Module Worker (modern browsers)
const worker = new Worker('worker.js', { type: 'module' });

4.2. Main Thread API

// Khởi tạo
const worker = new Worker('worker.js');

// Gửi message
worker.postMessage(data);

// Nhận message
worker.onmessage = (event) => { /* ... */ };
// hoặc
worker.addEventListener('message', (event) => { /* ... */ });

// Xử lý lỗi
worker.onerror = (error) => { /* ... */ };

// Terminate worker
worker.terminate();

4.3. Worker Thread API

// Nhận message từ main thread
self.onmessage = (event) => {
  const data = event.data;
  // Process...
};

// Gửi message về main thread
self.postMessage(result);

// Import scripts khác
importScripts('helper1.js', 'helper2.js');

// Tự đóng worker
self.close();

4.4. Transferable Objects

Để tối ưu hiệu suất với dữ liệu lớn, sử dụng Transferable Objects:

// Main Thread
const buffer = new ArrayBuffer(1024 * 1024 * 100); // 100MB

// Transfer ownership (không copy)
worker.postMessage(buffer, [buffer]);
// Sau đây buffer.byteLength === 0 (đã transfer)

// Worker Thread
self.onmessage = (e) => {
  const receivedBuffer = e.data;
  // Process buffer...
  
  // Transfer ngược lại
  self.postMessage(receivedBuffer, [receivedBuffer]);
};

Các Transferable Objects:

  • ArrayBuffer
  • MessagePort
  • ImageBitmap
  • OffscreenCanvas

5. Use Cases cụ thể

5.1. 🖼️ Xử lý Hình ảnh

Vấn đề: Xử lý ảnh (resize, filter, compression) block UI

Giải pháp:

// main.js
const imageWorker = new Worker('image-worker.js');

function processImage(imageData) {
  return new Promise((resolve) => {
    imageWorker.postMessage({
      type: 'APPLY_FILTER',
      imageData: imageData,
      filter: 'grayscale'
    });
    
    imageWorker.onmessage = (e) => resolve(e.data);
  });
}
// image-worker.js
self.onmessage = (e) => {
  const { type, imageData, filter } = e.data;
  
  if (type === 'APPLY_FILTER') {
    const processed = applyFilter(imageData, filter);
    self.postMessage(processed);
  }
};

function applyFilter(imageData, filter) {
  const data = imageData.data;
  
  if (filter === 'grayscale') {
    for (let i = 0; i < data.length; i += 4) {
      const avg = (data[i] + data[i+1] + data[i+2]) / 3;
      data[i] = data[i+1] = data[i+2] = avg;
    }
  }
  
  return imageData;
}

5.2. 📊 Xử lý Dữ liệu Lớn (Data Processing)

Vấn đề: Parse/transform dataset lớn (CSV, JSON) gây freeze UI

Giải pháp:

// data-worker.js
self.onmessage = async (e) => {
  const { type, data } = e.data;
  
  switch (type) {
    case 'PARSE_CSV':
      const rows = parseCSV(data);
      self.postMessage({ type: 'PARSED', rows });
      break;
      
    case 'AGGREGATE':
      const result = aggregateData(data);
      self.postMessage({ type: 'AGGREGATED', result });
      break;
      
    case 'FILTER':
      const filtered = data.filter(row => row.value > 100);
      self.postMessage({ type: 'FILTERED', filtered });
      break;
  }
};

function parseCSV(csvString) {
  const lines = csvString.split('\n');
  const headers = lines[0].split(',');
  
  return lines.slice(1).map(line => {
    const values = line.split(',');
    return headers.reduce((obj, header, i) => {
      obj[header.trim()] = values[i]?.trim();
      return obj;
    }, {});
  });
}

5.3. 🔐 Encryption/Decryption

Vấn đề: Mã hóa/giải mã dữ liệu là tác vụ CPU-intensive

Giải pháp:

// crypto-worker.js
self.onmessage = async (e) => {
  const { type, data, key } = e.data;
  
  try {
    if (type === 'ENCRYPT') {
      const encrypted = await encryptData(data, key);
      self.postMessage({ success: true, result: encrypted });
    } else if (type === 'DECRYPT') {
      const decrypted = await decryptData(data, key);
      self.postMessage({ success: true, result: decrypted });
    }
  } catch (error) {
    self.postMessage({ success: false, error: error.message });
  }
};

async function encryptData(data, key) {
  const encoder = new TextEncoder();
  const dataBuffer = encoder.encode(JSON.stringify(data));
  
  const cryptoKey = await crypto.subtle.importKey(
    'raw',
    encoder.encode(key),
    { name: 'AES-GCM' },
    false,
    ['encrypt']
  );
  
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encrypted = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    cryptoKey,
    dataBuffer
  );
  
  return { iv, encrypted };
}

5.4. 🧮 Tính toán Phức tạp (Heavy Computation)

Vấn đề: Thuật toán phức tạp (Machine Learning, Path Finding, Physics Simulation)

Giải pháp:

// computation-worker.js
self.onmessage = (e) => {
  const { type, params } = e.data;
  
  switch (type) {
    case 'FIBONACCI':
      const fib = fibonacci(params.n);
      self.postMessage({ result: fib });
      break;
      
    case 'PRIME_NUMBERS':
      const primes = findPrimes(params.limit);
      self.postMessage({ result: primes });
      break;
      
    case 'SORT_LARGE_ARRAY':
      const sorted = mergeSort(params.array);
      self.postMessage({ result: sorted });
      break;
  }
};

// Thuật toán tìm số nguyên tố
function findPrimes(limit) {
  const sieve = new Array(limit + 1).fill(true);
  sieve[0] = sieve[1] = false;
  
  for (let i = 2; i * i <= limit; i++) {
    if (sieve[i]) {
      for (let j = i * i; j <= limit; j += i) {
        sieve[j] = false;
      }
    }
  }
  
  return sieve.reduce((primes, isPrime, num) => {
    if (isPrime) primes.push(num);
    return primes;
  }, []);
}

5.5. 🌐 Background API Calls & Sync

Vấn đề: Fetch và xử lý dữ liệu API lớn gây lag

Giải pháp:

// api-worker.js
self.onmessage = async (e) => {
  const { type, url, options } = e.data;
  
  try {
    if (type === 'FETCH_AND_PROCESS') {
      // Fetch data
      const response = await fetch(url, options);
      const data = await response.json();
      
      // Process in background
      const processed = processData(data);
      
      // Report progress
      self.postMessage({ 
        type: 'PROGRESS', 
        message: 'Processing complete' 
      });
      
      // Return result
      self.postMessage({ 
        type: 'COMPLETE', 
        result: processed 
      });
    }
  } catch (error) {
    self.postMessage({ 
      type: 'ERROR', 
      error: error.message 
    });
  }
};

function processData(data) {
  // Transform, aggregate, filter...
  return data.map(item => ({
    ...item,
    computed: item.value * 2,
    timestamp: new Date().toISOString()
  }));
}

5.6. 🎮 Real-time Collaboration

Vấn đề: Sync dữ liệu real-time giữa nhiều users

Giải pháp với Shared Worker:

// collaboration-shared-worker.js
const clients = new Set();
let sharedState = {};

self.onconnect = (e) => {
  const port = e.ports[0];
  clients.add(port);
  
  // Gửi state hiện tại cho client mới
  port.postMessage({ type: 'INIT', state: sharedState });
  
  port.onmessage = (event) => {
    const { type, data } = event.data;
    
    if (type === 'UPDATE') {
      // Cập nhật shared state
      sharedState = { ...sharedState, ...data };
      
      // Broadcast đến tất cả clients
      clients.forEach(client => {
        client.postMessage({ type: 'STATE_CHANGED', state: sharedState });
      });
    }
  };
  
  port.start();
};

6. Best Practices

6.1. ✅ Tối ưu Communication

// ❌ Bad: Gửi nhiều messages nhỏ
for (let i = 0; i < 1000; i++) {
  worker.postMessage({ index: i, value: data[i] });
}

// ✅ Good: Batch data
worker.postMessage({ 
  type: 'BATCH_PROCESS',
  data: data.slice(0, 1000) 
});

6.2. ✅ Sử dụng Transferable Objects

// ❌ Bad: Copy buffer (chậm với data lớn)
worker.postMessage(largeBuffer);

// ✅ Good: Transfer ownership
worker.postMessage(largeBuffer, [largeBuffer]);

6.3. ✅ Worker Pool Pattern

class WorkerPool {
  constructor(workerScript, poolSize = 4) {
    this.workers = [];
    this.taskQueue = [];
    this.availableWorkers = [];
    
    for (let i = 0; i < poolSize; i++) {
      const worker = new Worker(workerScript);
      this.workers.push(worker);
      this.availableWorkers.push(worker);
    }
  }
  
  execute(task) {
    return new Promise((resolve, reject) => {
      const runTask = (worker) => {
        worker.onmessage = (e) => {
          resolve(e.data);
          this.releaseWorker(worker);
        };
        worker.onerror = (e) => {
          reject(e);
          this.releaseWorker(worker);
        };
        worker.postMessage(task);
      };
      
      if (this.availableWorkers.length > 0) {
        runTask(this.availableWorkers.pop());
      } else {
        this.taskQueue.push({ task, resolve, reject, runTask });
      }
    });
  }
  
  releaseWorker(worker) {
    if (this.taskQueue.length > 0) {
      const { runTask } = this.taskQueue.shift();
      runTask(worker);
    } else {
      this.availableWorkers.push(worker);
    }
  }
  
  terminate() {
    this.workers.forEach(w => w.terminate());
  }
}

// Sử dụng
const pool = new WorkerPool('computation-worker.js', 4);

const results = await Promise.all([
  pool.execute({ type: 'TASK_1', data: data1 }),
  pool.execute({ type: 'TASK_2', data: data2 }),
  pool.execute({ type: 'TASK_3', data: data3 }),
]);

6.4. ✅ Error Handling

// Main Thread
worker.onerror = (error) => {
  console.error('Worker error:', error.message);
  console.error('File:', error.filename);
  console.error('Line:', error.lineno);
  
  // Restart worker nếu cần
  worker.terminate();
  worker = new Worker('worker.js');
};

// Worker Thread
self.onmessage = (e) => {
  try {
    const result = riskyOperation(e.data);
    self.postMessage({ success: true, result });
  } catch (error) {
    self.postMessage({ 
      success: false, 
      error: error.message,
      stack: error.stack 
    });
  }
};

6.5. ✅ Cleanup Resources

// Terminate worker khi không cần
useEffect(() => {
  const worker = new Worker('worker.js');
  
  return () => {
    worker.terminate(); // Cleanup khi component unmount
  };
}, []);

7. Hạn chế

7.1. ❌ Không thể truy cập DOM

// Worker Thread - Sẽ ERROR
self.onmessage = () => {
  // ❌ Không có document
  document.getElementById('app'); // ReferenceError
  
  // ❌ Không có window
  window.localStorage; // ReferenceError
};

7.2. ❌ Same-Origin Policy

Workers phải được load từ cùng origin với trang chủ.

// ❌ Không thể load worker từ CDN khác domain
const worker = new Worker('https://cdn.example.com/worker.js'); // Error

7.3. ❌ Overhead cho task nhỏ

// ❌ Không nên dùng worker cho task nhỏ
// Overhead của việc tạo worker + serialize message > thời gian xử lý

// Task này nên chạy trên main thread
const sum = numbers.reduce((a, b) => a + b, 0);

7.4. ❌ Debug khó hơn

  • Không thể đặt breakpoint trực tiếp trong một số IDE
  • Console.log trong worker hiển thị khác
  • Stack trace phức tạp hơn

7.5. ❌ Giới hạn API

Có thể sử dụng:

  • fetchXMLHttpRequest
  • WebSockets
  • IndexedDB
  • WebCrypto
  • setTimeoutsetInterval
  • Promiseasync/await

KHÔNG thể sử dụng:

  • DOM API (document, window)
  • localStoragesessionStorage
  • alert()confirm()prompt()
  • Một số Canvas API (ngoại trừ OffscreenCanvas)

8. Ví dụ thực tế

8.1. React Hook – useWorker

// hooks/useWorker.js
import { useEffect, useRef, useCallback, useState } from 'react';

export function useWorker(workerScript) {
  const workerRef = useRef(null);
  const [isLoading, setIsLoading] = useState(false);
  const [result, setResult] = useState(null);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    workerRef.current = new Worker(workerScript);
    
    workerRef.current.onmessage = (e) => {
      setResult(e.data);
      setIsLoading(false);
    };
    
    workerRef.current.onerror = (e) => {
      setError(e.message);
      setIsLoading(false);
    };
    
    return () => {
      workerRef.current?.terminate();
    };
  }, [workerScript]);
  
  const postMessage = useCallback((data) => {
    setIsLoading(true);
    setError(null);
    workerRef.current?.postMessage(data);
  }, []);
  
  return { postMessage, result, isLoading, error };
}

// Sử dụng
function DataProcessor() {
  const { postMessage, result, isLoading } = useWorker('/workers/data-worker.js');
  
  const handleProcess = () => {
    postMessage({ type: 'PROCESS', data: largeDataset });
  };
  
  return (
    <div>
      <button onClick={handleProcess} disabled={isLoading}>
        {isLoading ? 'Processing...' : 'Process Data'}
      </button>
      {result && <pre>{JSON.stringify(result, null, 2)}</pre>}
    </div>
  );
}

8.2. Vite/Webpack Worker Configuration

// Vite - Native support
const worker = new Worker(
  new URL('./worker.js', import.meta.url),
  { type: 'module' }
);

// Webpack 5 - Native support
const worker = new Worker(
  new URL('./worker.js', import.meta.url)
);

8.3. TypeScript Worker

// worker.types.ts
interface WorkerMessage {
  type: 'PROCESS' | 'CANCEL';
  data?: unknown;
}

interface WorkerResponse {
  success: boolean;
  result?: unknown;
  error?: string;
}

// data-worker.ts
const ctx: Worker = self as unknown as Worker;

ctx.onmessage = (e: MessageEvent<WorkerMessage>) => {
  const { type, data } = e.data;
  
  if (type === 'PROCESS') {
    try {
      const result = processData(data);
      ctx.postMessage({ success: true, result } as WorkerResponse);
    } catch (error) {
      ctx.postMessage({ 
        success: false, 
        error: (error as Error).message 
      } as WorkerResponse);
    }
  }
};

9. Kết luận

9.1. Khi nào NÊN dùng Web Workers

✅ Nên dùngVí dụ
Xử lý dữ liệu lớnParse CSV/JSON > 1MB
Tính toán phức tạpMachine Learning, Pathfinding
Xử lý mediaImage/Video processing
EncryptionEnd-to-end encryption
Real-time syncCollaborative editing

9.2. Khi nào KHÔNG nên dùng

❌ Không nênLý do
DOM manipulationWorkers không truy cập được DOM
Task < 50msOverhead > benefit
Simple calculationsKhông cần thiết
Heavy UI updatesVẫn phải về main thread

📚 Tham khảo

  1. MDN Web Docs – Web Workers API
  2. web.dev – Workers Overview
  3. JavaScript.info – Web Workers

Print
Image

enqtran

I'm enqtran - A coder and blogger :) [email protected]