Skip to content

Web Workers & Service Workers — Multi-Threaded JavaScript Guide

DodaTech Updated 2026-06-24 6 min read

In this tutorial, you'll learn about Web Workers & Service Workers. We cover key concepts, practical examples, and best practices.

Web Workers run JavaScript in background threads, keeping the UI responsive during heavy computation. Service Workers act as programmable network proxies, enabling offline support, caching, push notifications, and background sync.

In this tutorial, you'll learn to use Web Workers for CPU-intensive tasks without freezing the UI, and Service Workers for offline-first experiences, asset caching, and push notifications. By the end, you'll build applications that stay responsive under heavy load and work without a network connection.

Real-world use: Durga Antivirus Pro uses a Web Worker to compute file hashes (MD5, SHA256) for threat signature matching without blocking the UI. A Service Worker caches virus definition databases for offline scanning and intercepts network requests to block known malicious domains before they reach the browser.

flowchart TD
  A[Main Thread] --> B[UI Rendering]
  A --> C[User Events]
  A --> D[Web Worker]
  D --> E[Heavy Computation]
  D --> F[File Hashing]
  D --> G[Data Processing]
  A --> H[Service Worker]
  H --> I[Cache Assets]
  H --> J[Intercept Requests]
  H --> K[Push Notifications]
  H --> L[Background Sync]
  style D fill:#4af,color:#fff
  style H fill:#f90,color:#fff

Web Workers — Dedicated Background Threads

A Web Worker runs a separate JavaScript file in its own thread.

// main.js
const worker = new Worker('hash-worker.js');

worker.postMessage({
  file: '/home/docs/report.pdf',
  algorithm: 'sha256'
});

worker.onmessage = (event) => {
  const { hash, duration } = event.data;
  console.log(`SHA256: ${hash}`);
  console.log(`Computed in ${duration}ms`);
  updateHashDisplay(hash);
};

worker.onerror = (error) => {
  console.error('Worker error:', error.message);
};
// hash-worker.js
self.onmessage = async (event) => {
  const { file, algorithm } = event.data;
  const startTime = performance.now();

  try {
    const response = await fetch(file);
    const buffer = await response.arrayBuffer();
    const hashBuffer = await crypto.subtle.digest(algorithm, buffer);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');

    self.postMessage({
      hash: hashHex,
      duration: performance.now() - startTime
    });
  } catch (error) {
    self.postMessage({ error: error.message });
  }
};

Expected output:

SHA256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Computed in 342ms

Worker Communication Patterns

Workers communicate via postMessage and receive messages via the message event.

// main.js — transferable objects for zero-copy performance
const worker = new Worker('processor.js');
const largeBuffer = new ArrayBuffer(1024 * 1024 * 100);

worker.postMessage({ buffer: largeBuffer }, [largeBuffer]);
// largeBuffer is now empty in the main thread — ownership transferred

// main.js — shared buffer for concurrent access
const sharedBuffer = new SharedArrayBuffer(1024);
const sharedArray = new Int32Array(sharedBuffer);
worker.postMessage({ shared: sharedBuffer });

Terminating Workers

Clean up workers when they're no longer needed.

// From the main thread
worker.terminate();

// From inside the worker
self.close();

Service Workers — Network Proxy

A Service Worker intercepts network requests and enables offline functionality.

// sw.js
const CACHE_NAME = 'dodatech-cache-v1';
const ASSETS = [
  '/',
  '/styles/main.css',
  '/scripts/app.js',
  '/offline.html'
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(ASSETS);
    })
  );
  self.skipWaiting();
});

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();
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cachedResponse) => {
      if (cachedResponse) {
        return cachedResponse;
      }

      return fetch(event.request).then((response) => {
        if (response.status === 200) {
          const clone = response.clone();
          caches.open(CACHE_NAME).then((cache) => {
            cache.put(event.request, clone);
          });
        }
        return response;
      }).catch(() => {
        return caches.match('/offline.html');
      });
    })
  );
});

Registering a Service Worker

// Register from the main page
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js', {
    scope: '/'
  }).then((registration) => {
    console.log('SW registered:', registration.scope);

    registration.addEventListener('updatefound', () => {
      const newWorker = registration.installing;
      newWorker.addEventListener('statechange', () => {
        if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
          showUpdateNotification();
        }
      });
    });
  }).catch((error) => {
    console.error('SW registration failed:', error);
  });
}

Push Notifications

Send push notifications from the server through the Service Worker.

// sw.js — listen for push events
self.addEventListener('push', (event) => {
  const data = event.data.json();

  const options = {
    body: data.body,
    icon: '/icon-192.png',
    badge: '/badge-72.png',
    data: { url: data.url },
    actions: [
      { action: 'view', title: 'View Details' },
      { action: 'dismiss', title: 'Dismiss' }
    ]
  };

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

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

Background Sync

Sync data when the network becomes available.

// sw.js
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-threats') {
    event.waitUntil(syncThreatReports());
  }
});

async function syncThreatReports() {
  const db = await openIndexedDB();
  const pending = await db.getAll('pending-threats');

  for (const report of pending) {
    await fetch('/api/threats/report', {
      method: 'POST',
      body: JSON.stringify(report)
    });
    await db.delete('pending-threats', report.id);
  }
}
// Register a sync from the page
navigator.serviceWorker.ready.then((registration) => {
  registration.sync.register('sync-threats');
});

Web Worker vs Service Worker

Feature Web Worker Service Worker
Purpose Background computation Network proxy, caching
DOM access No No
Lifecycle Page-bound Independent, persists
Terminate Can be terminated Browser manages
API access Most Web APIs Limited (no DOM, no localStorage)
Can block page unload No Yes (extendable events)
Number per page Unlimited One per scope

Common Errors

  1. Service Worker scope too narrow — A Service Worker at /scripts/sw.js can only intercept requests under /scripts/. Register it at the root (/sw.js) for full-site coverage.

  2. Not handling worker errors — Uncaught errors in Web Workers silently fail. Always add an onerror handler and wrap worker code in try-catch blocks.

  3. Using alert() or confirm() in workers — Workers cannot access the DOM. This includes alert(), confirm(), localStorage, and document. Use postMessage to communicate back to the main thread.

  4. Forgetting to update the cache — Old Service Worker caches accumulate. Use the activate event to delete outdated caches by name.

  5. Service Worker not updating — The browser waits for all tabs to close before activating a new Service Worker. Use self.skipWaiting() and clients.claim() for immediate activation.

Practice Questions

  1. What's the difference between a Web Worker and a Service Worker? Web Workers execute scripts in background threads for computation. Service Workers act as programmable network proxies with persistent lifecycle, supporting offline caching, push notifications, and background sync.

  2. Can Web Workers access the DOM? No. Workers run in a separate global context (self or DedicatedWorkerGlobalScope) with no access to document, window, or DOM APIs.

  3. How do you update a Service Worker? Update the sw.js file. The browser detects the byte difference during the next navigation. The new worker installs, waits in waiting state, and activates after all tabs close or when skipWaiting() is called.

  4. What are Transferable Objects and when should you use them? Transferable objects (like ArrayBuffer) can be passed to workers without copying — ownership moves to the worker. Use them for large data (megabytes+) to avoid duplicating memory.

  5. Why does a Service Worker need HTTPS? Service Workers have powerful interception capabilities. HTTPS ensures the Service Worker script hasn't been tampered with during delivery. Localhost is exempt for development.

Challenge

Build a file scanning web app that uses a Web Worker to compute SHA256 hashes of uploaded files without freezing the UI. Show a progress indicator for files larger than 100MB. The Service Worker should cache the hashing library and definition files, and queue scan results for upload when offline using Background Sync.

Real-World Task

Implement the offline scanning architecture for Durga Antivirus Pro web dashboard. Use a Web Worker to compute file hashes for threat signature matching — process 500MB files without UI lag. Use a Service Worker to cache the virus definition database (updated daily via push), intercept API requests to known malicious domains, and queue threat reports with Background Sync when the network is unavailable.


Previous: JavaScript Async/Await | Next: Progressive Web Apps | Related: Web Performance

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro