092
LVL 04 — SENIOR-IN-TRAININGSESSION 092DAY 92

WEB WORKERS

🎫 PIXELCRAFT-078
Performance | 🔴 Expert | Priority: 🔴 Critical

Even with throttling, applying filters to 4K images blocks the main thread for 200ms+ — enough for visible jank. Move filter processing to a Web Worker. The main thread stays free for UI. Sliders never lag again.
CONCEPTS.UNLOCKED
⚙️
Web Workers
Separate threads for heavy computation. JavaScript is single-threaded — but Web Workers run on separate CPU cores. The main thread handles UI; the worker handles math. No more jank.
📨
postMessage / onmessage
Communication between threads. Main thread sends data to the worker with postMessage. Worker processes it and sends results back. Async, event-driven — like a mini microservice.
🚀
Transferable Objects
Move data without copying (zero-copy). Normally postMessage clones data. With transferable objects, the ArrayBuffer is moved — ownership transfers instantly. No copy, no overhead.
🛡️
Main Thread Protection
Never do heavy computation on the UI thread. The main thread must stay under 50ms per task to maintain 60fps. Anything heavier → offload to a worker. The UI stays responsive always.
🧵
SharedArrayBuffer
Shared memory between threads. Both main thread and worker read/write the same memory. Faster than postMessage but requires synchronization (Atomics). Advanced — use with caution.
⚖️
When Workers Are Overkill
Workers have overhead. Spawning a worker + serializing data + deserializing results. For operations under 16ms, the overhead exceeds the benefit. Profile first — don't assume you need a worker.
HANDS-ON.TASKS
01
Create the Filter Worker
// filter-worker.js self.onmessage = function(event) { const { imageData, filters, width, height } = event.data; const pixels = new Uint8ClampedArray(imageData); for (let i = 0; i < pixels.length; i += 4) { let [r, g, b] = [ pixels[i], pixels[i+1], pixels[i+2]]; for (const filter of filters) { [r, g, b] = applyFilterFn(r, g, b, filter); } pixels[i] = clamp(r); pixels[i+1] = clamp(g); pixels[i+2] = clamp(b); } // Transfer buffer back (zero-copy) self.postMessage( { pixels: pixels.buffer }, [pixels.buffer] ); }; function applyFilterFn( r, g, b, filter ) { switch (filter.name) { case 'brightness': return [r + filter.value, g + filter.value, b + filter.value]; case 'grayscale': { const avg = (r + g + b) / 3; return [avg, avg, avg]; } case 'invert': return [255-r, 255-g, 255-b]; default: return [r, g, b]; } } function clamp(v) { return Math.min(255, Math.max(0, Math.round(v))); }
02
useFilterWorker Hook
function useFilterWorker() { const workerRef = useRef<Worker | null>(null); useEffect(() => { workerRef.current = new Worker( new URL( './filter-worker.js', import.meta.url), { type: 'module' } ); return () => workerRef.current?.terminate(); }, []); const processFilters = useCallback(( imageData: ImageData, filters: Filter[] ): Promise<Uint8ClampedArray> => { return new Promise((resolve) => { const worker = workerRef.current!; worker.onmessage = (event) => { const result = new Uint8ClampedArray( event.data.pixels); resolve(result); }; // Transfer buffer (zero-copy) const buffer = imageData.data.buffer.slice(0); worker.postMessage( { imageData: buffer, filters, width: imageData.width, height: imageData.height, }, [buffer] // transferable list ); }); }, []); return processFilters; }
The second argument to postMessage is the transferable list. These ArrayBuffers are moved, not copied — ownership transfers from main thread to worker (and back) with zero overhead.
03
Integrate into the Editor
function CanvasArea({ image, filters }: { image: ImageData | null; filters: Record<string, number>; }) { const canvasRef = useRef<HTMLCanvasElement>(null); const processFilters = useFilterWorker(); useEffect(() => { if (!image) return; const filterList = Object.entries(filters) .map(([name, value]) => ({ name, value })); processFilters(image, filterList) .then(pixels => { const ctx = canvasRef.current! .getContext('2d')!; const result = new ImageData( pixels, image.width, image.height); ctx.putImageData(result, 0, 0); }); }, [image, filters]); return <canvas ref={canvasRef} />; }
04
Benchmark: Before vs After
// Test: move sliders rapidly // on a 4K image (3840 × 2160) // BEFORE (main thread): // Main thread blocked: ~200ms // UI jank: visible stutter // FPS during filter: 5fps // AFTER (Web Worker): // Main thread blocked: 0ms // UI jank: none // FPS during filter: 60fps // Worker processing: ~200ms // (same time, different thread)
The total computation time doesn't change — it's still ~200ms. But it happens on a different CPU core. The main thread stays free for UI at 60fps the entire time.
05
Close the Ticket
git switch -c perf/PIXELCRAFT-078-web-workers git add src/ git commit -m "Move filter processing to Web Worker (PIXELCRAFT-078)" git push origin perf/PIXELCRAFT-078-web-workers # PR → Review → Merge → Close ticket ✅
CS.DEEP-DIVE

Concurrency is not parallelism.

JavaScript's event loop provides concurrency (juggling tasks on one thread). Web Workers provide parallelism (running on separate CPU cores simultaneously).

// Concurrency vs parallelism:

Concurrency (event loop)
  One thread, many tasks
  Switches between them quickly
  Feels simultaneous, isn't

Parallelism (Web Workers)
  Multiple threads, multiple cores
  Actually simultaneous
  True parallel execution

// Same pattern everywhere:
GPU rendering
  GPU handles pixels, CPU handles logic
Microservices
  Separate services, separate concerns
OS processes
  Process isolation, separate memory

// True parallelism introduces:
// synchronization, data races,
// and communication overhead.
// Worth it when the work is heavy.
"Worker Lab"
[A]Add a worker pool: spawn 4 workers (one per core). Split the image into 4 horizontal strips, process each strip on a different worker, reassemble. Measure the speedup — does 4 workers = 4× faster?
[B]Add progress reporting: have the worker postMessage progress updates (25%, 50%, 75%) during processing. Show a progress bar in the UI while the worker computes. Real-time feedback without blocking.
[C]Research: what is SharedArrayBuffer and when would you use it? How does Atomics.wait/notify work? What are the security implications (Spectre) that require Cross-Origin-Isolation headers?
REF.MATERIAL
ARTICLE
MDN Web Docs
Official Web Worker guide: creating workers, message passing, transferable objects, shared workers, and error handling.
WORKERSOFFICIALESSENTIAL
VIDEO
Fireship
Quick overview of Web Workers: when to use them, postMessage, transferable objects, and practical examples.
WORKERSQUICK
ARTICLE
Surma (Google)
Deep analysis of postMessage performance: structured cloning cost, transferable objects, and when the overhead matters. Spoiler: transferables make it fast.
PERFORMANCEDEEP DIVEESSENTIAL
VIDEO
Rob Pike (Go creator)
The classic talk explaining the difference between concurrency and parallelism. Essential CS knowledge from the creator of Go.
CONCURRENCYCS
ARTICLE
Google
Architecture patterns for offloading work from the main thread. Comlink library for easier worker communication.
WORKERSARCHITECTURE
// LEAVE EXCITED BECAUSE
The UI never freezes, ever. A 200ms filter computation on 4K images happens invisibly on a separate thread. Sliders are buttery smooth. This is how professional editors like Photoshop work.