DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

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 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();
Enter fullscreen mode Exit fullscreen mode
// 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);
}
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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]);
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
// 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]);
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Module Workers

Workers can use ES modules (Firefox 114+):

// ESM worker
const worker = new Worker('/worker.js', { type: 'module' });
Enter fullscreen mode Exit fullscreen mode
// worker.js as ES module
import { processData } from './utils.js';
import * as Comlink from 'comlink';

Comlink.expose({ processData });
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
// 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();
};
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

Workers have message passing overhead — for very small operations, the overhead exceeds the benefit. Workers are valuable for operations >100ms.

Top comments (0)