跪拜 Guibai
← Back to the summary

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

Web Workers Don't Render: Thread Division and Message Loops in React

Abstract: The key to this React Demo isn't simply "enabling multi-threading," but rather splitting responsibilities: the main thread handles the page, the Worker handles computation, postMessage handles transmission, useRef holds the instance, and useEffect manages the lifecycle. This article is based on local source code review; the code has not been run and verified.

When a React component executes a large loop, what's actually occupied is the page's main thread. The Event Loop can schedule asynchronous tasks, but it won't automatically parallelize the current large loop. A more appropriate approach here is to use a Web Worker and treat the Worker as an external resource that needs to be managed.

The Core Judgment of This Demo

Don't write the Worker inside the component function body, otherwise there's a risk of repeated creation on every re-render. You should:

useEffect creates
useRef saves
postMessage sends tasks
onmessage receives results
cleanup terminate

Creating a Thread and Saving an Instance Are Two Separate Things

const workerRef = useRef(null)

useEffect(() => {
  workerRef.current = new Worker(
    new URL('../worker.js', import.meta.url)
  )
}, [])

The easiest point of confusion here: useRef does not create the thread; new Worker() actually creates the thread. The ref merely provides a stable { current } container.

The Worker instance doesn't need to participate in page rendering, so it's unsuitable to save it with state. A button click can retrieve the same instance:

workerRef.current.postMessage({ num: 88 })

Message Protocol: Two postMessages, Opposite Directions

The main thread sends:

workerRef.current.postMessage({
  num: 88,
})

The Worker receives and computes:

self.onmessage = (e) => {
  const { num } = e.data
  let sum = 0

  for (let i = 0; i < 5000000000; i++) {
    sum += num * i
  }

  self.postMessage({ result: sum })
}

The data returned by the Worker is received by the main thread:

workerRef.current.onmessage = (e) => {
  setResult(e.data.result)
  setLoading(false)
}

Therefore, remember:

Code Direction
worker.postMessage(data) Main thread to Worker
self.onmessage Worker receives main thread message
self.postMessage(data) Worker to main thread
worker.onmessage Main thread receives Worker message

Why Can't a Worker Directly Modify the Page?

The Worker and the page's main thread are independent of each other; it cannot directly access React components or the DOM. It only returns plain messages:

self.postMessage({ result: sum })

After the main thread receives it, it executes:

setResult(e.data.result)

Page updates still belong to the React main thread's responsibility. In other words, the Worker's value is "offloading computation," not "replacing rendering."

Lifecycle Cleanup Is a Necessary Step

After a Worker is created, its termination must be considered:

useEffect(() => {
  const worker = new Worker(
    new URL('../worker.js', import.meta.url)
  )

  return () => {
    worker.terminate()
  }
}, [])

Calling terminate() when the component unmounts stops the background thread. In real code, workerRef.current would also be set to null, indicating that no usable instance currently exists.

What Else Should This Example Check?

Several boundary conditions in the static code need further verification:

  1. The page prompt says "five hundred million iterations," but the actual loop limit is 5000000000, i.e., 5 billion iterations.
  2. The calculation result may exceed JavaScript's safe integer range; a regular Number cannot guarantee a precise integer.
  3. Currently, no onerror or onmessageerror is seen; the loading state after a Worker failure needs runtime verification.
  4. If the result is rendered conditionally using result &&, it won't display when the result is 0.

These are source code review conclusions and do not represent reproduction in a browser; the code in this article has not been run and verified.

Check Sequence When Reusing This Design

Is it a CPU-intensive task?
  ↓ Yes
Can it be split into a pure computation function that doesn't need the DOM?
  ↓ Yes
useEffect creates Worker
  ↓
useRef saves Worker
  ↓
Agree on input and output message structures
  ↓
onmessage updates state
  ↓
cleanup terminate

Conclusion

The key to using Web Workers in React isn't about moving all asynchronous work to a new thread, but identifying CPU computations that genuinely block the main thread and establishing a clear message protocol. useRef handles instance saving, useEffect handles resource lifecycle, useState handles UI state, and the Worker handles computational isolation. The next step could be adding error messages and task cancellation capabilities to the Demo, then verifying behavior under different input scales.