Offloading Heavy React Computation to Web Workers Without Freezing the UI
A single expensive computation — a large loop, image processing, or game logic — can lock the entire browser tab. Moving that work to a Web Worker keeps the UI interactive and avoids the perception of a broken app, and the useRef + useEffect pattern prevents memory leaks and double-initialization bugs that are easy to introduce in React.
JavaScript's single-threaded model freezes the page when a long-running loop hogs the main thread; the Event Loop only postpones tasks, it doesn't shrink them. Web Workers run scripts in a separate thread with no DOM access, making them the right tool for pure computation, fetch calls, or IndexedDB work that would otherwise block rendering. The standard React integration pattern uses useRef to hold the Worker instance across re-renders and useEffect with an empty dependency array to create the Worker after the first paint, so the UI appears before the thread spins up. Communication between the main thread and the Worker happens exclusively through postMessage and onmessage, with both sides exchanging plain objects via e.data. On component unmount, terminate() kills the thread and the ref is set to null to surface any accidental postMessage calls as hard errors instead of silent failures.
The mental model that separates Event Loop (queuing) from Web Workers (true parallelism) is often blurred in practice, and this breakdown makes the boundary explicit: one reorders work on the same thread, the other adds a new thread.
Setting the Worker ref to null after terminate() is a defensive pattern that converts a silent failure into a loud error — a small detail that prevents hard-to-diagnose bugs in production.
The insistence on useEffect for Worker creation is really about scheduling priority: the first paint wins, and the Worker is treated as a deferred side effect, which is a useful framing for any expensive initialization in React.