Steven's Knowledge
Advanced Features

Workers

Web Workers, Service Workers, SharedWorker, and Worklets — off-main-thread computation, offline support, and background processing

Workers

Worker APIs allow JavaScript to run in background threads, separate from the main UI thread. This prevents heavy computation from blocking user interactions and enables offline-capable, resilient web applications.

Problems Solved

ProblemHow Workers Solve It
Heavy computation blocks UI (jank)Web Workers run in separate thread
No offline support in web appsService Workers cache and serve assets
No shared state between tabsSharedWorker shares a single worker across tabs
Audio/visual processing blocks main threadWorklets run on specialized threads
Push notifications require app openService Workers receive push events in background

Worker Types

Browser Process
├── Main Thread
│   ├── DOM rendering
│   ├── Event handling
│   └── JavaScript execution

├── Web Worker Thread (per worker)
│   └── Heavy computation, data processing

├── SharedWorker Thread (shared across tabs)
│   └── Cross-tab state, connection pooling

├── Service Worker Thread (per scope)
│   └── Network interception, caching, push

└── Worklet Threads (specialized)
    ├── AudioWorklet → Real-time audio processing
    ├── PaintWorklet → Custom CSS painting
    └── AnimationWorklet → Smooth animations

Web Workers

Basic Usage

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

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

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

worker.onerror = (event) => {
  console.error('Worker error:', event.message);
};

// Terminate when done
worker.terminate();
// worker.js
self.onmessage = (event) => {
  const { type, data } = event.data;

  if (type === 'process') {
    // Heavy computation runs here without blocking UI
    const result = data.map(item => expensiveOperation(item));
    self.postMessage(result);
  }
};

Inline Worker (No Separate File)

function createInlineWorker(fn) {
  const blob = new Blob(
    [`self.onmessage = function(e) { (${fn.toString()})(e); }`],
    { type: 'application/javascript' }
  );
  const url = URL.createObjectURL(blob);
  const worker = new Worker(url);

  // Clean up blob URL when worker is terminated
  const originalTerminate = worker.terminate.bind(worker);
  worker.terminate = () => {
    URL.revokeObjectURL(url);
    originalTerminate();
  };

  return worker;
}

const worker = createInlineWorker((e) => {
  const result = e.data.reduce((sum, n) => sum + n, 0);
  self.postMessage(result);
});

worker.postMessage([1, 2, 3, 4, 5]);
worker.onmessage = (e) => console.log('Sum:', e.data); // 15

Transferable Objects

Transfer ownership of data instead of copying — zero-copy for large buffers:

// main.js — Transfer an ArrayBuffer (zero-copy)
const buffer = new ArrayBuffer(1024 * 1024 * 100); // 100MB
console.log(buffer.byteLength); // 104857600

// Transfer moves the buffer to the worker; main thread loses access
worker.postMessage({ buffer }, [buffer]);
console.log(buffer.byteLength); // 0 — ownership transferred

// worker.js
self.onmessage = (e) => {
  const { buffer } = e.data;
  const view = new Float64Array(buffer);
  // Process the data...
  self.postMessage({ buffer }, [buffer]); // Transfer back
};

Transferable Types

TypeTransferableStructured Clone
ArrayBufferYesYes
MessagePortYesNo
ReadableStreamYesNo
WritableStreamYesNo
ImageBitmapYesNo
OffscreenCanvasYesNo
Plain objects/arraysNoYes
FunctionsNoNo
DOM nodesNoNo

Worker Pool

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

  exec(data) {
    return new Promise((resolve, reject) => {
      const task = { data, resolve, reject };
      const available = this.workers.find(w => !this.activeWorkers.has(w));

      if (available) {
        this._dispatch(available, task);
      } else {
        this.queue.push(task);
      }
    });
  }

  _dispatch(worker, task) {
    this.activeWorkers.add(worker);

    worker.onmessage = (e) => {
      this.activeWorkers.delete(worker);
      task.resolve(e.data);

      // Process next item in queue
      if (this.queue.length > 0) {
        this._dispatch(worker, this.queue.shift());
      }
    };

    worker.onerror = (e) => {
      this.activeWorkers.delete(worker);
      task.reject(e);
    };

    worker.postMessage(task.data);
  }

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

// Usage
const pool = new WorkerPool('image-processor.js', 4);

const results = await Promise.all(
  images.map(img => pool.exec({ image: img, operation: 'resize' }))
);

Use Cases

Use CaseExample
Data processingCSV parsing, JSON transformation, sorting
Image manipulationFilters, resizing, format conversion
EncryptionHashing, encryption/decryption
Search/indexingFull-text search, fuzzy matching
CompilationMarkdown rendering, code compilation
SimulationPhysics, pathfinding, game logic

SharedWorker

A SharedWorker is shared across all tabs/windows from the same origin. Each page connects through a MessagePort.

// shared-worker.js
const connections = new Set();

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

  port.onmessage = (event) => {
    // Broadcast to all connected tabs
    connections.forEach(p => {
      if (p !== port) {
        p.postMessage(event.data);
      }
    });
  };

  port.onclose = () => connections.delete(port);
  port.start();
};
// page.js (any tab)
const worker = new SharedWorker('shared-worker.js');
const port = worker.port;

port.onmessage = (e) => {
  console.log('Message from another tab:', e.data);
};

port.postMessage({ action: 'sync', data: 'Hello from this tab' });

SharedWorker Use Cases

  • Cross-tab state synchronization — shared shopping cart, user presence
  • Single WebSocket connection — one connection shared across tabs
  • Connection pooling — share database or API connections
  • Deduplication — prevent duplicate API calls from multiple tabs

Service Workers

Service Workers act as a programmable network proxy between the browser and network. They enable offline support, background sync, and push notifications.

Lifecycle

Service Worker Lifecycle
1. Register          navigator.serviceWorker.register('/sw.js')
2. Install           Cache essential assets
3. Activate          Clean up old caches
4. Idle / Fetch      Intercept network requests
5. Terminate         Browser can stop idle workers to save memory
6. Update            New SW version detected → install → activate

Registration

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

      // Listen for updates
      reg.onupdatefound = () => {
        const newWorker = reg.installing;
        newWorker.onstatechange = () => {
          if (newWorker.state === 'activated') {
            console.log('New version activated');
          }
        };
      };
    })
    .catch(err => console.error('SW registration failed:', err));
}

Caching Strategies

// sw.js
const CACHE_NAME = 'app-v1';
const PRECACHE_URLS = ['/', '/styles.css', '/app.js', '/offline.html'];

// Install: precache essential assets
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(PRECACHE_URLS))
      .then(() => self.skipWaiting())
  );
});

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

// Fetch: serve from cache, fall back to network
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then(cached => {
      if (cached) return cached;

      return fetch(event.request).then(response => {
        // Cache successful GET responses
        if (response.ok && event.request.method === 'GET') {
          const clone = response.clone();
          caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
        }
        return response;
      }).catch(() => {
        // Offline fallback for navigation requests
        if (event.request.mode === 'navigate') {
          return caches.match('/offline.html');
        }
      });
    })
  );
});

Common Caching Strategies

StrategyHow It WorksBest For
Cache FirstCheck cache → fall back to networkStatic assets (CSS, JS, images)
Network FirstTry network → fall back to cacheAPI responses, frequently updated content
Stale While RevalidateServe cache immediately → update cache in backgroundBalance of speed and freshness
Network OnlyAlways fetch from networkNon-cacheable requests, real-time data
Cache OnlyOnly serve from cachePrecached assets in offline mode

Stale While Revalidate

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.open(CACHE_NAME).then(cache =>
      cache.match(event.request).then(cached => {
        const fetchPromise = fetch(event.request).then(response => {
          cache.put(event.request, response.clone());
          return response;
        });
        return cached || fetchPromise;
      })
    )
  );
});

Background Sync

// main.js — register sync when offline
async function saveData(data) {
  try {
    await fetch('/api/data', { method: 'POST', body: JSON.stringify(data) });
  } catch {
    // Save to IndexedDB for later
    await saveToIndexedDB('pending-syncs', data);
    const reg = await navigator.serviceWorker.ready;
    await reg.sync.register('sync-data');
  }
}

// sw.js — process when back online
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-data') {
    event.waitUntil(
      getFromIndexedDB('pending-syncs').then(items =>
        Promise.all(items.map(item =>
          fetch('/api/data', { method: 'POST', body: JSON.stringify(item) })
        ))
      ).then(() => clearIndexedDB('pending-syncs'))
    );
  }
});

Push Notifications

// main.js — subscribe to push
async function subscribeToPush() {
  const reg = await navigator.serviceWorker.ready;
  const subscription = await reg.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
  });
  // Send subscription to server
  await fetch('/api/push/subscribe', {
    method: 'POST',
    body: JSON.stringify(subscription),
  });
}

// sw.js — receive push
self.addEventListener('push', (event) => {
  const data = event.data?.json() || {};
  event.waitUntil(
    self.registration.showNotification(data.title || 'Notification', {
      body: data.body,
      icon: '/icon-192.png',
      data: { url: data.url },
    })
  );
});

self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  const url = event.notification.data?.url || '/';
  event.waitUntil(clients.openWindow(url));
});

Worklets

Worklets are lightweight workers that run on specialized browser threads for audio, paint, and animation processing.

Paint Worklet (CSS Houdini)

// checkerboard-painter.js
class CheckerboardPainter {
  static get inputProperties() {
    return ['--checker-size', '--checker-color'];
  }

  paint(ctx, geom, properties) {
    const size = parseInt(properties.get('--checker-size')) || 20;
    const color = properties.get('--checker-color').toString().trim() || '#000';

    for (let y = 0; y < geom.height; y += size) {
      for (let x = 0; x < geom.width; x += size) {
        if ((Math.floor(x / size) + Math.floor(y / size)) % 2 === 0) {
          ctx.fillStyle = color;
          ctx.fillRect(x, y, size, size);
        }
      }
    }
  }
}

registerPaint('checkerboard', CheckerboardPainter);
// main.js
CSS.paintWorklet.addModule('checkerboard-painter.js');
.checkerboard {
  --checker-size: 30;
  --checker-color: #e2e8f0;
  background-image: paint(checkerboard);
}

Audio Worklet

// noise-processor.js
class WhiteNoiseProcessor extends AudioWorkletProcessor {
  process(inputs, outputs) {
    const output = outputs[0];
    output.forEach(channel => {
      for (let i = 0; i < channel.length; i++) {
        channel[i] = Math.random() * 2 - 1;
      }
    });
    return true; // Keep processor alive
  }
}

registerProcessor('white-noise', WhiteNoiseProcessor);
// main.js
const audioCtx = new AudioContext();
await audioCtx.audioWorklet.addModule('noise-processor.js');

const noiseNode = new AudioWorkletNode(audioCtx, 'white-noise');
noiseNode.connect(audioCtx.destination);

Worker Communication Summary

PatternMechanismDirection
Main ↔ WorkerpostMessage / onmessageBidirectional
Main ↔ SharedWorkerMessagePortBidirectional
Main → Service Workernavigator.serviceWorker.controller.postMessage()One-way
Service Worker → Mainclient.postMessage()One-way
Worker ↔ WorkerMessageChannelBidirectional
Any context broadcastBroadcastChannelMulticast

Best Practices

  • Use Transferable Objects for large data (ArrayBuffer, ImageBitmap) to avoid copy overhead
  • Implement a Worker Pool for CPU-intensive parallel tasks
  • Service Workers require HTTPS (except localhost)
  • Always handle the statechange event to detect Service Worker updates
  • Use skipWaiting() + clients.claim() for immediate activation when appropriate
  • Clean up old caches in the activate event
  • Keep Service Worker code minimal — the browser may terminate idle workers at any time

On this page