Skip to content

JavaScript Workers — Web Workers, Service Workers, and Parallel Execution

DodaTech Updated 2026-06-29 5 min read

In this tutorial, you will learn about JavaScript Workers. We cover key concepts, practical examples, and best practices to help you master this topic.

JavaScript is single-threaded by default, but workers provide genuine parallelism. Web Workers run CPU-intensive tasks off the main thread. Service Workers act as programmable proxies between the browser and the network, enabling offline support, push notifications, and background sync.

DodaTech uses Service Workers for offline dashboard access and Web Workers for processing large security scan datasets without freezing the UI.

What You'll Learn

  • Web Workers: communication, life cycle, use cases
  • Service Workers: registration, Caching strategies, offline support
  • Shared Workers
  • Transferable objects (performance)
  • Worker best practices
  • Debugging workers

Web Workers

// main.js
const worker = new Worker("scan-worker.js");

// Send data to worker
worker.postMessage({
  type: "process",
  data: largeArray
});

// Receive results
worker.onmessage = (event) => {
  console.log("Result:", event.data);
};

// Handle errors
worker.onerror = (error) => {
  console.error("Worker error:", error.message);
};

// Terminate when done
worker.terminate();

// scan-worker.js
self.onmessage = (event) => {
  const { type, data } = event.data;

  switch (type) {
    case "process":
      const result = data.map(item => heavyComputation(item));
      self.postMessage(result);
      break;
    case "cancel":
      self.close(); // Terminate this worker
      break;
  }
};

function heavyComputation(item) {
  // CPU-intensive work
  let result = 0;
  for (let i = 0; i < 1000000; i++) {
    result += Math.sqrt(item * i);
  }
  return result;
}

Worker Pool Pattern

class WorkerPool {
  constructor(workerScript, size = navigator.hardwareConcurrency) {
    this.workers = [];
    this.taskQueue = [];
    this.availableWorkers = [];
    this.callbacks = new Map();
    this.idCounter = 0;

    for (let i = 0; i < size; i++) {
      const worker = new Worker(workerScript);
      worker.onmessage = (event) => this.#handleResult(event);
      worker.idle = true;
      this.workers.push(worker);
      this.availableWorkers.push(worker);
    }
  }

  #handleResult(event) {
    const { taskId, result } = event.data;
    const callback = this.callbacks.get(taskId);
    if (callback) {
      callback(result);
      this.callbacks.delete(taskId);
    }

    // Worker is now idle
    const worker = this.workers.find(
      w => w.idle === false && w.taskId === taskId
    );
    if (worker) {
      worker.idle = true;
      this.availableWorkers.push(worker);
    }

    // Process next task
    this.#processNext();
  }

  #processNext() {
    if (this.taskQueue.length === 0 || this.availableWorkers.length === 0) return;

    const worker = this.availableWorkers.pop();
    const task = this.taskQueue.shift();
    worker.idle = false;
    worker.taskId = task.id;
    worker.postMessage({ taskId: task.id, data: task.data });
  }

  runTask(data) {
    return new Promise((resolve) => {
      const id = ++this.idCounter;
      this.callbacks.set(id, resolve);
      this.taskQueue.push({ id, data });
      this.#processNext();
    });
  }

  terminate() {
    this.workers.forEach(w => w.terminate());
    this.workers = [];
    this.availableWorkers = [];
  }
}

Service Workers

Registration

// main.js
if ("serviceWorker" in navigator) {
  window.addEventListener("load", async () => {
    try {
      const registration = await navigator.serviceWorker.register(
        "/service-worker.js",
        { scope: "/" }
      );
      console.log("SW registered:", registration.scope);

      // Check for updates
      registration.onupdatefound = () => {
        const newWorker = registration.installing;
        newWorker.onstatechange = () => {
          if (newWorker.state === "installed") {
            if (navigator.serviceWorker.controller) {
              console.log("Update available!");
              // Notify user to refresh
            }
          }
        };
      };
    } catch (err) {
      console.error("SW registration failed:", err);
    }
  });
}

Caching Strategies

// service-worker.js
const CACHE_NAME = "v1";
const STATIC_ASSETS = [
  "/",
  "/index.html",
  "/app.js",
  "/styles.css",
  "/logo.png"
];

// Install: cache static assets
self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(STATIC_ASSETS);
    })
  );
  // Activate immediately without waiting
  self.skipWaiting();
});

// Activate: clean old caches
self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then((keys) => {
      return Promise.all(
        keys
          .filter((key) => key !== CACHE_NAME)
          .map((key) => caches.delete(key))
      );
    })
  );
  clients.claim(); // Take control immediately
});

// Fetch strategies
self.addEventListener("fetch", (event) => {
  const { request } = event;
  const url = new URL(request.url);

  // Strategy 1: Cache First (for static assets)
  if (STATIC_ASSETS.includes(url.pathname)) {
    event.respondWith(cacheFirst(request));
    return;
  }

  // Strategy 2: Network First (for API calls)
  if (url.pathname.startsWith("/api/")) {
    event.respondWith(networkFirst(request));
    return;
  }

  // Strategy 3: Stale While Revalidate (default)
  event.respondWith(staleWhileRevalidate(request));
});

async function cacheFirst(request) {
  const cached = await caches.match(request);
  return cached || fetch(request);
}

async function networkFirst(request) {
  try {
    const response = await fetch(request);
    const cache = await caches.open(CACHE_NAME);
    cache.put(request, response.clone());
    return response;
  } catch (err) {
    const cached = await caches.match(request);
    if (cached) return cached;
    // Return offline page
    return caches.match("/offline.html");
  }
}

async function staleWhileRevalidate(request) {
  const cache = await caches.open(CACHE_NAME);
  const cached = await cache.match(request);
  const fetchPromise = fetch(request).then((response) => {
    cache.put(request, response.clone());
    return response;
  });
  return cached || fetchPromise;
}

Push Notifications

// service-worker.js
self.addEventListener("push", (event) => {
  const data = event.data.json();
  const options = {
    body: data.body,
    icon: "/icon.png",
    badge: "/badge.png",
    data: { url: data.url }
  };

  event.waitUntil(
    self.registration.showNotification(data.title, options)
  );
});

self.addEventListener("notificationclick", (event) => {
  event.notification.close();
  event.waitUntil(
    clients.openWindow(event.notification.data.url)
  );
});

Transferable Objects

For large data, avoid copying — transfer ownership instead:

// main.js
const largeBuffer = new ArrayBuffer(1000000);

// Without transfer: 1MB is copied to worker
worker.postMessage({ data: largeBuffer });

// With transfer: 1MB is moved (main thread loses access)
worker.postMessage({ data: largeBuffer }, [largeBuffer]);
// largeBuffer.byteLength === 0 now

// Transferable types:
// - ArrayBuffer
// - MessagePort
// - ReadableStream
// - WritableStream
// - TransformStream
// - OffscreenCanvas
// - ImageBitmap

Best Practices

// 1. Terminate unused workers
const worker = new Worker("worker.js");
// ... use it ...
worker.terminate();

// 2. Error handling
worker.onerror = (event) => {
  console.error("Uncaught error in worker:", event.message);
  // Offer fallback on main thread
};

// 3. Keep messages serializable (no functions, DOM nodes)
worker.postMessage({ numbers: [1, 2, 3] }); // OK
// worker.postMessage(() => {}); // Error!

// 4. Use importScripts in workers
// worker.js:
// importScripts("math-utils.js", "data-processor.js");

// 5. Module workers (newer):
// const worker = new Worker("worker.js", { type: "module" });

Practice Questions

  1. Implement a Web Worker that computes prime numbers up to N.

  2. Write a Service Worker that implements a "Network First with timeout" Strategy.

  3. Create a SharedWorker that maintains a counter visible to multiple tabs.

  4. Implement a cache-busting strategy in a Service Worker that updates assets when a version changes.

  5. Write a worker that processes image data using OffscreenCanvas (compute brightness histogram).

Challenge: Offline-First Dashboard Shell

Build a complete Service Worker for a dashboard app:

  • Cache HTML shell and static assets on install
  • Cache API responses with network-first strategy
  • Show offline notification when network is lost
  • Queue failed API requests for replay when online
  • Background sync for pending mutations
  • Version management with cache invalidation

This mirrors DodaTech's approach for the dashboard — ensuring security teams can review scan results even without network connectivity.

Real-World Task: Data Export Worker

Write a Web Worker that handles client-side data export:

  • Receives raw JSON data
  • Transforms to CSV, XLSX, or PDF format
  • Supports progress reporting to main thread
  • Handles cancellation
  • Uses transferable objects for large datasets
  • Supports filters, sorting, column selection

DodaTech's data export feature uses this exact pattern — allowing users to export millions of scan results without freezing the browser.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro