JavaScript Workers — Web Workers, Service Workers, and Parallel Execution
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
Implement a Web Worker that computes prime numbers up to N.
Write a Service Worker that implements a "Network First with timeout" Strategy.
Create a SharedWorker that maintains a counter visible to multiple tabs.
Implement a cache-busting strategy in a Service Worker that updates assets when a version changes.
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