跪拜 Guibai
← All articles
JavaScript · React.js

React Web Workers: The Thread Doesn't Render, It Just Ships Results Back

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

Offloading CPU work to a Worker keeps a React UI responsive, but the real risk is architectural: storing a Worker in component state or creating it on every render leaks threads and breaks the message contract. This pattern pins down exactly which hook owns which responsibility.

Summary

Running a 5-billion-iteration loop in a React component freezes the UI because the Event Loop won't parallelize synchronous work. The fix is a Web Worker, but the architecture matters more than the thread itself. The Worker instance lives in a `useRef` — not state, since it never drives rendering — and gets created inside a `useEffect` that also handles `terminate()` on cleanup. A two-way `postMessage` protocol sends a task payload from main thread to Worker, and the Worker ships a plain result object back; the main thread then calls `setState` to update the UI. The Worker never touches the DOM or React components, so rendering stays firmly on the main thread.

Static code review surfaces four bugs worth checking at runtime: the UI says 500 million iterations but the loop runs 5 billion, the result can overflow JavaScript's safe integer range, there's no `onerror` handler to clear the loading state on failure, and a falsy result of `0` won't render under a truthiness guard. A decision flowchart — CPU-bound? Pure computation? — helps decide when this pattern applies.

Takeaways
Create the Worker inside `useEffect` to avoid re-instantiation on every render.
Store the Worker instance in a `useRef`, not in state, because it never participates in rendering.
Use `postMessage` to send data from main thread to Worker, and `worker.onmessage` to receive results back.
The Worker returns plain data; only the main thread calls `setState` to update the UI.
Call `worker.terminate()` in the `useEffect` cleanup function to stop the background thread when the component unmounts.
The demo's UI says 500 million iterations, but the loop actually runs 5 billion — a mismatch caught by static review.
A result of `0` won't render if the component uses a truthiness check like `result &&`.
Missing `onerror` and `onmessageerror` handlers mean a Worker failure leaves the loading state stuck.
Conclusions

The pattern's value isn't multi-threading — it's treating a Worker as an external resource with explicit creation, messaging, and teardown, the same way you'd manage a database connection.

Storing the Worker in `useRef` rather than state is a deliberate signal: this object exists outside React's render cycle and should never trigger a re-render.

The static code review catches four concrete bugs without running the code, which is a useful reminder that reading source can surface integer-overflow, truthiness, and error-handling gaps before a browser does.

The decision flowchart — CPU-bound? Pure computation? — is a practical filter that prevents developers from over-applying Workers to tasks the Event Loop already handles well.

Concepts & terms
Web Worker
A browser API that runs scripts in a background thread separate from the main UI thread. Workers cannot access the DOM directly and communicate with the main thread via `postMessage`.
useRef
A React hook that returns a mutable object whose `.current` property persists across renders without triggering re-renders when changed. Ideal for holding non-render values like timer IDs or Worker instances.
postMessage protocol
The structured messaging contract between main thread and Worker: `worker.postMessage(data)` sends from main to Worker; `self.postMessage(data)` sends from Worker back to main. Each side listens with `onmessage`.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗