DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

Web Workers: Move Heavy Computation Off the Main Thread in Firefox

JavaScript is single-threaded, which means heavy computation blocks the UI — no smooth animations, no responsive inputs, no updates while processing. Web Workers run JavaScript in a background thread, keeping the main thread free. Firefox has full Web Worker support including SharedArrayBuffer and Atomics.

Creating a Web Worker

Workers are created from a JavaScript file:

// main.js
const worker = new Worker('/workers/math.js');

// Send message to worker
worker.postMessage({ type: 'compute', n: 1000000 });

// Receive message from worker
worker.onmessage = (event) => {
  console.log('Result:', event.data.result);
};

worker.onerror = (error) => {
  console.error('Worker error:', error.message);
};
Enter fullscreen mode Exit fullscreen mode
// workers/math.js
self.onmessage = (event) => {
  if (event.data.type === 'compute') {
    const result = heavyComputation(event.data.n);
    self.postMessage({ result });
  }
};

function heavyComputation(n) {
  let sum = 0;
  for (let i = 0; i < n; i++) {
    sum += Math.sqrt(i) * Math.sin(i);
  }
  return sum;
}
Enter fullscreen mode Exit fullscreen mode

postMessage serializes data using the Structured Clone algorithm — more powerful than JSON (supports TypedArrays, Maps, Sets, RegExp, etc.).

Transferable Objects (Zero-Copy)

For large data (ArrayBuffers, ImageBitmaps), use transferables to avoid copying:

// Without transfer: copies the buffer (slow for large data)
const buffer = new ArrayBuffer(1000000);
worker.postMessage({ buffer });

// With transfer: moves ownership to worker (zero copy)
worker.postMessage({ buffer }, [buffer]);
// After transfer, 'buffer' is detached in main thread — can't use it anymore
Enter fullscreen mode Exit fullscreen mode

The worker receives the buffer and can transfer it back:

// In worker
self.onmessage = (event) => {
  const { buffer } = event.data;
  const view = new Float32Array(buffer);
  // Process...
  self.postMessage({ buffer }, [buffer]); // Transfer back
};
Enter fullscreen mode Exit fullscreen mode

Use Cases

Image Processing

// Expensive: blur, resize, filter operations
const imageWorker = new Worker('/workers/image-processor.js');

function processImage(imageData) {
  return new Promise((resolve) => {
    imageWorker.postMessage(
      { imageData: imageData.data.buffer, width: imageData.width, height: imageData.height },
      [imageData.data.buffer] // Transfer the pixel data
    );
    imageWorker.onmessage = (e) => resolve(new ImageData(e.data.result, e.data.width));
  });
}
Enter fullscreen mode Exit fullscreen mode

Large Data Parsing

// Parse a large CSV/JSON file without freezing the UI
const parseWorker = new Worker('/workers/parser.js');

async function parseLargeFile(fileContent) {
  return new Promise((resolve, reject) => {
    parseWorker.postMessage({ content: fileContent });
    parseWorker.onmessage = (e) => resolve(e.data.records);
    parseWorker.onerror = (e) => reject(e);
  });
}

// In worker:
// self.onmessage = (e) => {
//   const records = parseCSV(e.data.content);
//   self.postMessage({ records });
// }
Enter fullscreen mode Exit fullscreen mode

Cryptographic Operations

// Hash passwords, generate keys, encrypt data
const cryptoWorker = new Worker('/workers/crypto.js');

async function hashPassword(password) {
  return new Promise((resolve) => {
    cryptoWorker.postMessage({ type: 'hash', password });
    cryptoWorker.onmessage = (e) => resolve(e.data.hash);
  });
}
Enter fullscreen mode Exit fullscreen mode

Worker Module Syntax

Modern workers support ES modules:

// main.js
const worker = new Worker('/workers/modern.js', { type: 'module' });
Enter fullscreen mode Exit fullscreen mode
// workers/modern.js (ES module worker)
import { heavyCalculation } from '../utils/math.js';

self.onmessage = async (event) => {
  const result = await heavyCalculation(event.data);
  self.postMessage({ result });
};
Enter fullscreen mode Exit fullscreen mode

Shared Workers

Share a worker between multiple tabs/windows:

// In each tab/window:
const shared = new SharedWorker('/workers/shared.js');

shared.port.start();
shared.port.postMessage({ type: 'subscribe', channel: 'updates' });
shared.port.onmessage = (event) => {
  console.log('Update from shared worker:', event.data);
};
Enter fullscreen mode Exit fullscreen mode
// workers/shared.js
const ports = new Set();

self.onconnect = (event) => {
  const port = event.ports[0];
  ports.add(port);
  port.start();

  port.onmessage = (e) => {
    // Broadcast to all connected tabs
    ports.forEach(p => p.postMessage({ broadcast: e.data }));
  };
};
Enter fullscreen mode Exit fullscreen mode

Shared Workers are useful for: shared state across tabs, deduplicating WebSocket connections, shared cache management.

OffscreenCanvas: Rendering in a Worker

Move canvas rendering to a worker:

// main.js
const canvas = document.getElementById('myCanvas');
const offscreen = canvas.transferControlToOffscreen();

const renderWorker = new Worker('/workers/renderer.js');
renderWorker.postMessage({ canvas: offscreen }, [offscreen]);
Enter fullscreen mode Exit fullscreen mode
// workers/renderer.js
self.onmessage = (event) => {
  const { canvas } = event.data;
  const ctx = canvas.getContext('2d');

  function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    // Expensive drawing operations...
    requestAnimationFrame(draw);
  }
  draw();
};
Enter fullscreen mode Exit fullscreen mode

This is perfect for games and data visualizations — the rendering runs on a worker thread without competing with the main thread.

Worker Pool Pattern

Reuse workers instead of creating/destroying them:

class WorkerPool {
  constructor(workerUrl, size = navigator.hardwareConcurrency || 4) {
    this.workers = Array.from({ length: size }, () => ({
      worker: new Worker(workerUrl),
      busy: false,
    }));
    this.queue = [];
  }

  run(data) {
    return new Promise((resolve, reject) => {
      const task = { data, resolve, reject };
      const freeWorker = this.workers.find(w => !w.busy);

      if (freeWorker) {
        this.dispatch(freeWorker, task);
      } else {
        this.queue.push(task);
      }
    });
  }

  dispatch(entry, task) {
    entry.busy = true;
    entry.worker.postMessage(task.data);
    entry.worker.onmessage = (e) => {
      task.resolve(e.data);
      entry.busy = false;
      if (this.queue.length > 0) {
        this.dispatch(entry, this.queue.shift());
      }
    };
    entry.worker.onerror = (e) => {
      task.reject(e);
      entry.busy = false;
    };
  }
}

// Usage
const pool = new WorkerPool('/workers/processor.js');
const results = await Promise.all(items.map(item => pool.run(item)));
Enter fullscreen mode Exit fullscreen mode

Debugging Workers in Firefox

Firefox DevTools shows workers in the Debugger:

  1. Open DevTools → Debugger
  2. In the left panel, find your worker scripts under "Workers"
  3. Set breakpoints in worker code just like main thread code
  4. The worker's thread is listed separately in the "Threads" panel
  5. Each thread can be paused and stepped through independently

console.log() in workers outputs to the regular Browser Console in Firefox.

Top comments (0)