Web Workers in React for CPU-Intensive Background Tasks0%

Web Workers in React for CPU-Intensive Background Tasks

Beginner12 min read•Updated: Jul 11, 2026
Study MaterialsCheatsheet

Web Workers in React for CPU-Intensive Background Tasks

JavaScript in the browser executes on a single main thread. If your React application performs heavy CPU computations—such as parsing a 100MB CSV file, generating image filters, running cryptographic hashing, or executing complex machine learning models—the main thread blocks, completely freezing user input, animations, and transitions. Web Workers offload these heavy calculations to isolated background OS threads.


1. The Main Thread Bottleneck

Output
Main Thread (UI, React Reconciliation, User Clicks):
───[Click]───[Typing]───► [ 3.5s Heavy Math Loop ] ───► [UI Frozen / Frames Dropped] ───►
 
With Web Worker:
Main Thread: ───[Click]───► [Post Message] ───► [UI Stays Smooth 60 FPS] ───► [Receive Result]
Worker Thread: ──────────────────────────► [ 3.5s Heavy Math Loop ] ────────────►

2. Setting Up Vite Web Workers with Modern Syntax

Modern Vite and Webpack 5 support native Web Worker instantiation using standard ES module syntax:

TypeScript
// src/workers/heavyComputation.worker.ts
// Worker running on separate background thread
 
self.onmessage = (event: MessageEvent<{ rawData: number[] }>) => {
const { rawData } = event.data;
 
// CPU-heavy calculation: e.g. prime factorizations or statistical variance
let sum = 0;
for (let i = 0; i < rawData.length; i++) {
for (let j = 0; j < 1000; j++) {
sum += Math.sqrt(rawData[i] * j);
}
}
 
// Send result back to main React thread
self.postMessage({ result: sum });
};
 
export {};

3. Creating a Custom React Hook for Web Worker Integration

Managing worker instantiation, termination, and message listeners is best abstracted into a clean React hook:

TSX
import { useState, useEffect, useRef, useCallback } from "react";
 
export function useHeavyCalculation() {
const [result, setResult] = useState<number | null>(null);
const [isComputing, setIsComputing] = useState(false);
const workerRef = useRef<Worker | null>(null);
 
useEffect(() => {
// Instantiate worker using Vite's new Worker(new URL(..., import.meta.url))
workerRef.current = new Worker(
new URL("../workers/heavyComputation.worker.ts", import.meta.url),
{ type: "module" }
);
 
workerRef.current.onmessage = (event: MessageEvent<{ result: number }>) => {
setResult(event.data.result);
setIsComputing(false);
};
 
return () => {
// Terminate background OS thread on unmount to prevent memory leaks
workerRef.current?.terminate();
};
}, []);
 
const runCalculation = useCallback((data: number[]) => {
if (!workerRef.current) return;
setIsComputing(true);
workerRef.current.postMessage({ rawData: data });
}, []);
 
return { result, isComputing, runCalculation };
}

4. Consuming the Worker in a React Component

TSX
import React, { useState } from "react";
import { useHeavyCalculation } from "./hooks/useHeavyCalculation";
 
export function WorkerBenchmarkView() {
const [clickCount, setClickCount] = useState(0);
const { result, isComputing, runCalculation } = useHeavyCalculation();
 
const handleCompute = () => {
const syntheticData = Array.from({ length: 50000 }, () => Math.random() * 100);
runCalculation(syntheticData);
};
 
return (
<div className="p-8 bg-slate-900 text-white min-h-screen">
<h1 className="text-xl font-bold mb-4">Web Worker Multithreaded Processing</h1>
 
<div className="flex gap-4 items-center mb-6">
<button
onClick={handleCompute}
disabled={isComputing}
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 disabled:bg-slate-700 rounded font-semibold"
>
{isComputing ? "Computing on Worker Thread..." : "Trigger 50M Math Cycles"}
</button>
 
{/* UI Responsiveness Test Button */}
<button
onClick={() => setClickCount((c) => c + 1)}
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 rounded font-semibold"
>
Click Me Rapidly: {clickCount}
</button>
</div>
 
<div className="p-4 bg-slate-950 border border-slate-800 rounded">
<p className="text-sm text-slate-400">
Result:{" "}
<span className="text-cyan-400 font-mono">
{result !== null ? result.toLocaleString() : "Awaiting execution"}
</span>
</p>
</div>
</div>
);
}

Notice how clickCount increments instantly without a single dropped frame, even while the Web Worker executes 50 million math iterations!


5. Transferable Objects for Zero-Copy Memory

Passing huge buffers (ArrayBuffer, Uint8Array) via standard postMessage copies data across threads. Using Transferable Objects transfers ownership of the memory buffer directly with zero serialization cost:

TypeScript
const bigBuffer = new Uint8Array(1024 * 1024 * 100); // 100MB buffer
// Passing buffer in transfer list (second argument)
worker.postMessage({ buffer: bigBuffer }, [bigBuffer.buffer]);

Interactive Quiz

Practice Quiz

Question 1
What is the primary purpose of using Web Workers in a React application?
Question 2
Can a Web Worker directly manipulate React state or the DOM (document.getElementById)?
Question 3
Why is calling worker.terminate() inside useEffect's cleanup function critical?
Question 4
What are Transferable Objects in the Web Worker API?
Question 5
How do modern bundlers like Vite instantiate ES Module Web Workers?
Next Lesson

Optimizing React Re-render Bottlenecks with Fiber Architecture

Continue learning with hands-on practice, examples, and exercises in the upcoming topic.

Practice Quiz

Test your understanding of this lesson with 5 questions. Each question has one correct answer.

PrevNext