跪拜 Guibai
← All articles
JavaScript · Vue.js

Offloading Heavy React Computation to Web Workers Without Freezing the UI

By 嘟嘟0717 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

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.

Summary

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.

Takeaways
Event Loop asynchrony handles waiting (fetch, timers) but cannot stop a heavy synchronous loop from monopolizing the main thread.
Web Workers run in an independent thread with no document or window, but can use fetch, IndexedDB, timers, and Blob.
Store the Worker instance in useRef, not useState, because Worker changes should not trigger re-renders.
Create the Worker inside a useEffect with an empty dependency array so the component renders first and the Worker doesn't block the first paint.
Main thread and Worker communicate only via postMessage and onmessage; the data format is identical on both sides, only the direction differs.
Destructure e.data to extract only the fields needed, e.g., const { num } = e.data.
On unmount, call workerRef.current.terminate() to kill the thread, then set the ref to null so any stray postMessage call throws immediately.
Placing the Worker file in src/ and using new URL('./worker.js', import.meta.url) lets Vite compile and bundle it; placing it in public/ loads it as-is.
Conclusions

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.

Concepts & terms
Web Worker
A browser API that runs JavaScript in a separate background thread, with its own global scope (self) and no access to the DOM, document, or window. Used for CPU-intensive tasks to avoid blocking the main UI thread.
useRef
A React Hook that returns a mutable object whose .current property persists across renders without causing re-renders when changed. Ideal for holding non-UI values like Worker instances or interval IDs.
postMessage / onmessage
The message-passing API used for communication between the main thread and a Web Worker. postMessage sends data (copied via structured clone), and onmessage receives it inside a MessageEvent whose .data property holds the payload.
terminate()
A method on a Worker instance that immediately stops the Worker thread and reclaims its resources. Should be called in a cleanup function to prevent memory leaks when the owning component unmounts.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗