Web Workers in Firefox: Comlink, Transferable Objects, SharedArrayBuffer, and Module Workers
Web Workers allow JavaScript to run in background threads separate from the main thread. This prevents long-running operations from blocking the UI, keeping the page responsive. Firefox has excellent Web Worker support w
Web Workers allow JavaScript to run in background threads separate from the main thread. This prevents long-running operations from blocking the UI, keeping the page responsive. Firefox has excellent Web Worker support with great DevTools integration. This guide covers Web Workers, Shared Workers, and the Comlink library.
The Problem: The Single-Threaded Main Thread
JavaScript is single-threaded by design. Any expensive computation — sorting a large array, processing CSV data, image manipulation, encryption — blocks the main thread and freezes the UI.
Symptoms of main thread blocking:
- UI doesn't respond to clicks during the operation
- Animations stutter
- DevTools Performance panel shows long yellow "scripting" blocks
- Chrome/Firefox's "Page Unresponsive" dialog appears
Web Workers solve this by moving the computation to a background thread.
Creating a Basic Worker
// main.js (runs in page)
const worker = new Worker('/worker.js');
// Send message to worker
worker.postMessage({ type: 'PROCESS', data: largeArray });
// Receive result from worker
worker.onmessage = (event) => {
const result = event.data;
updateUI(result);
};
worker.onerror = (error) => {
console.error('Worker error:', error.message);
};
// Terminate the worker when done
worker.terminate();
// worker.js (background thread)
self.onmessage = (event) => {
const { type, data } = event.data;
if (type === 'PROCESS') {
// Heavy computation here - doesn't block UI
const result = processData(data);
self.postMessage(result);
}
};
function processData(data) {
// Expensive operation
return data.sort((a, b) => a.value - b.value);
}
Inline Workers with Blob URLs
Create workers without a separate file:
const workerCode = `
self.onmessage = (event) => {
const result = expensiveComputation(event.data);
self.postMessage(result);
};
function expensiveComputation(data) {
return data.map(x => x * 2);
}
`;
const blob = new Blob([workerCode], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
Transferable Objects (Zero-Copy)
Normally, postMessage() copies data (serialized with structured clone). For large binary data, transfer ownership instead — zero-copy:
const buffer = new ArrayBuffer(1024 * 1024 * 10); // 10MB
// Transfer ownership (buffer becomes empty in main thread)
worker.postMessage({ buffer }, [buffer]);
// buffer.byteLength === 0 now in main thread
// In worker, receive the transferred buffer
self.onmessage = (event) => {
const { buffer } = event.data;
// Process buffer
// Transfer back:
self.postMessage({ buffer }, [buffer]);
};
Transferable types: ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas.
Comlink: Cleaner Worker API
The raw postMessage API is verbose. Comlink wraps workers in a Proxy that makes async calls look like regular function calls:
// worker-api.js
import * as Comlink from 'comlink';
const api = {
async processData(data) {
return data.map(x => x * 2).filter(x => x > 10);
},
async fetchAndProcess(url) {
const response = await fetch(url);
const data = await response.json();
return this.processData(data);
}
};
Comlink.expose(api);
// main.js
import * as Comlink from 'comlink';
const worker = new Worker('./worker-api.js', { type: 'module' });
const api = Comlink.wrap(worker);
// Call worker functions as if they were local async functions
const result = await api.processData([1, 5, 15, 20]);
SharedArrayBuffer and Atomics
For workers that need to share memory (requires COOP/COEP headers):
// Create shared buffer
const shared = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 100);
const array = new Int32Array(shared);
// Send to worker (shared, not copied)
worker.postMessage({ shared });
// Atomics for thread-safe operations
Atomics.add(array, 0, 1); // thread-safe increment
Atomics.store(array, 1, 42); // thread-safe write
Atomics.load(array, 1); // thread-safe read
Atomics.wait(array, 0, 0); // block worker until value changes (workers only)
Firefox note: SharedArrayBuffer requires cross-origin isolation (COOP + COEP headers) in Firefox. If you see SharedArrayBuffer is not defined, add these response headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Module Workers
Workers can use ES modules (Firefox 114+):
// ESM worker
const worker = new Worker('/worker.js', { type: 'module' });
// worker.js as ES module
import { processData } from './utils.js';
import * as Comlink from 'comlink';
Comlink.expose({ processData });
Shared Workers
A SharedWorker is shared across all tabs/windows of the same origin:
// Each page connects to the same SharedWorker
const worker = new SharedWorker('/shared-worker.js');
worker.port.start();
worker.port.postMessage('hello');
worker.port.onmessage = (event) => console.log(event.data);
// shared-worker.js
const clients = new Set();
self.onconnect = (event) => {
const port = event.ports[0];
clients.add(port);
port.onmessage = (event) => {
// Broadcast to all connected tabs
clients.forEach(client => client.postMessage(event.data));
};
port.start();
};
Use cases: real-time sync between tabs, shared WebSocket connection, cached data shared across tabs.
Firefox DevTools for Workers
Console: worker console.log() appears in the main page's DevTools console with a "worker" label. Easy to identify.
Debugger: open DevTools → Debugger → the Sources panel shows worker files under "Workers". Set breakpoints directly in worker code. The debugger pauses inside the worker thread.
about:debugging → Workers: shows all running workers and their status.
Performance Measurement
// Benchmark without and with worker
const start = performance.now();
processData(largeArray); // main thread
console.log('Main thread:', performance.now() - start, 'ms');
// With worker:
const startW = performance.now();
worker.postMessage(largeArray);
worker.onmessage = () => {
console.log('Worker:', performance.now() - startW, 'ms');
// Note: includes serialization overhead
};
Workers have message passing overhead — for very small operations, the overhead exceeds the benefit. Workers are valuable for operations >100ms.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.