JavaScript is single-threaded. That's usually fine — but when you ask it to compress a 200MB video or process thousands of lines of text, the main thread blocks. The UI freezes. Animations stop. Buttons stop responding. Users think the page has crashed.
Web Workers solve this by moving heavy work off the main thread entirely. Here's how we use them in every EazyStudio tool.
The problem: the main thread does too much
The browser's main thread handles everything: rendering, user input, JavaScript execution, layout, and painting. When a long-running JavaScript task occupies this thread, everything else waits.
A 16ms frame budget (for 60fps) is all you have. A FFmpeg encode that takes 3 seconds blocks the main thread for 3 seconds — 180 missed frames. The page appears frozen.
Web Workers: a background thread for JavaScript
Web Workers run JavaScript in a separate thread with its own event loop. They can't access the DOM, but they can run arbitrary JavaScript — including WebAssembly modules.
// Create a worker
const worker = new Worker('/workers/audio-compress.js');
// Send file data to worker (zero-copy transfer)
worker.postMessage(
{ file: arrayBuffer, bitrate: 128 },
[arrayBuffer] // transferable: no copy made
);
// Receive result back on main thread
worker.addEventListener('message', (e) => {
if (e.data.type === 'progress') updateProgress(e.data.pct);
if (e.data.type === 'done') offerDownload(e.data.result);
});
Transferable objects: zero-copy performance
Normally, postMessage clones data — copying a 200MB ArrayBuffer would use 200MB more memory. Transferable objects solve this: the ArrayBuffer is moved (not copied) to the worker. The original reference becomes detached.
For large files, this is critical. Without transfers, processing a 500MB video would briefly require ~1GB of RAM just for the copy.
Progress reporting back to the UI
Workers communicate with the main thread via postMessage. We use this to stream progress updates while FFmpeg is running:
ffmpeg.setProgress(({ ratio }) => {
self.postMessage({ type: 'progress', pct: ratio * 100 });
});
await ffmpeg.run(...args);
const result = ffmpeg.readFile('output.mp3');
self.postMessage({ type: 'done', result }, [result.buffer]);