Ever clicked a button in a web app, and the UI just froze? Maybe a timer stopped, animations stuttered, or clickable elements stopped responding. This happens because JavaScript is single-threaded, meaning it can only do one thing at a time.
When a heavy computation runs on the main thread, it blocks the UI, creating a poor user experience.
In this blog, we’ll explore Web Workers, a simple and effective way to offload heavy computations to a background thread in a React app, keeping your UI smooth and responsive.
Understanding the problem: single-threaded JavaScript
JavaScript runs on a single thread that handles both:
- UI rendering
- JavaScript execution
This means if you run a heavy task, everything else stops until it finishes. Here’s a classic example:
// Blocks UI for several seconds
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const result = fibonacci(44);
console.log(result)
If this runs on the main thread, timers, animations, and clicks stop responding.
Introducing web workers
Web Workers allow us to run JavaScript code in a separate background thread. This way:
- Heavy tasks don’t freeze the UI
- You can continue interacting with the page
- Communication is done via postMessage() and onmessage()
Note: Workers cannot access the DOM directly; they are only for computations.
Web worker syntax
Worker File (public/worker.js):
self.onmessage = function (e) {
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const result = fibonacci(e.data);
self.postMessage(result);
};
Main Thread (React Component):
const workerRef = useRef(null);
useEffect(() => {
workerRef.current = new Worker("/worker.js");
workerRef.current.onmessage = (e) => {
alert('worker finished! Result : ',e.data);
};
return () => {
workerRef.current.terminate();
};
}, []);
const runWorkerTask = () => {
workerRef.current.postMessage(44);
};
React demo: without vs with web worker
Setting up a Next.js Project with Web Workers
1. Create Next.js App

2. Project Folder Structure
Here’s a minimal structure we’ll use:

Without worker (UI freezes)
Create WithoutWorker Component File (app/WithoutWorker.js)
'use client';
import { useEffect, useState } from 'react';
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
export default function WithoutWorker() {
const [timer, setTimer] = useState(0);
const [color, setColor] = useState('lightblue');
const [size, setSize] = useState(100);
useEffect(() => {
const interval = setInterval(() => setTimer((t) => t + 1), 1000);
return () => clearInterval(interval);
}, []);
const handleHeavyTask = async () => {
console.log('handleHeavyTask started...');
const result = fibonacci(44); // Heavy enough to lag most browsers
console.log('handleHeavyTask ended... : ', result);
};
const animateBox = () => {
// Try clicking this during a heavy task, no response
setColor((prev) => (prev === 'lightblue' ? 'lightcoral': 'lightblue'));
setSize((prev) => (prev === 100 ? 550 : 100));
};
return (
<div>
<h2>Without Web Worker</h2>
<p>Timer: {timer}</p>
<button
onClick={handleHeavyTask}
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
>
Run Heavy Task
</button>
<div
onClick={animateBox}
style={{
marginTop: '20px',
width: `${size}px`,
height: `${size}px`,
backgroundColor: colour,
cursor: 'pointer',
transition: 'all 0.3s ease',
}}
>
Click Me
</div>
</div>
);
}
Observation: Click the box while running the heavy task , nothing happens. The timer also freezes.

With worker (UI smooth)
Create Worker File (public/worker.js)
self.onmessage = function (e) {
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const result = fibonacci(44);
self.postMessage(result);
};
Create WithWorker Component File (app/WithWorker.js)
"use client"
import { useEffect, useRef, useState } from "react";
export default function WithWorker() {
const [timer, setTimer] = useState(0);
const [color, setColor] = useState("lightgreen");
const [size, setSize] = useState(100);
const workerRef = useRef(null);
useEffect(() => {
const interval = setInterval(() => setTimer(t => t + 1), 1000);
return () => clearInterval(interval);
}, []);
useEffect(() => { // Create the worker on mount
workerRef.current = new Worker("/worker.js"); // Creates a new background thread by loading our worker script.
workerRef.current.onmessage = (e) => { // Listens for the result from the worker.
console.log('handleHeavyTask ended... : ',e.data)
};
return () => {
workerRef.current.terminate(); // Cleans up the worker on component unmount.
};
}, []);
const runWorkerTask = () => {
console.log('handleHeavyTask started...')
workerRef.current.postMessage(44); // Sends a message to the worker to begin processing.
};
const animateBox = () => { // This should stay responsive even during heavy task
setColor((prev) => (prev === "lightgreen" ? "orange" : "lightgreen"));
setSize((prev) => (prev === 100 ? 550 : 100));
};
return (
<div>
<h2>With Web Worker</h2>
<p>Timer: {timer}</p>
<button
onClick={runWorkerTask}
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
>Run Heavy Task</button>
<div
onClick={animateBox}
style={{
marginTop: "20px",
width: `${size}px`,
height: `${size}px`,
backgroundColor: colour,
cursor: "pointer",
transition: "all 0.3s ease"
}}
>
Click Me
</div>
</div>
);
}
.png)
Observation: Click the box while the worker runs, the timer keeps going, and the box responds instantly!
Error handling
Handle Worker Errors in React
You can attach an onerror listener when creating the worker:
useEffect(() => { // Create the worker on mount
workerRef.current = new Worker("/worker.js"); // Creates a new background thread by loading our worker script.
workerRef.current.onmessage = (e) => { // Listens for the result from the worker.
console.log('handleHeavyTask ended... : ',e.data)
};
workerRef.current.onerror = (err) => { // Error handler
console.error("Worker error:", err.message);
alert("Something went wrong in the worker: " + err.message);
};
return () => {
workerRef.current.terminate(); // Cleans up the worker on component unmount.
};
}, []);
Catch Errors Inside Worker File
In your worker.js, wrap computation in a try/catch:
self.onmessage = function (e) {
try {
const n = e.data;
if (typeof n !== "number" || n < 0) {
throw new Error("Invalid input for Fibonacci");
}
function fibonacci(x) {
if (x <= 1) return x;
return fibonacci(x - 1) + fibonacci(x - 2);
}
const result = fibonacci(n);
self.postMessage(result);
} catch (err) {
// Send error back to main thread
self.postMessage({ error: err.message });
}
};
Key takeaways
- Web Workers allow true multithreading in the browser.
- Use them for CPU-intensive tasks like data processing, parsing large files, or complex calculations.
- Async/await only helps with I/O operations, not CPU-heavy work.
- Always clean up workers using .terminate() to avoid memory leaks.
Conclusion
Web Workers keep React apps responsive by moving heavy computations off the main thread, ensuring smooth user experiences even under load. They are already valuable in finance, simulations, and real-time communication, and their role will expand as WebAssembly, progressive web apps, and edge computing mature.
Countries like Singapore, South Korea, and Estonia are leading in digital infrastructure, while global research explores energy-efficient and privacy-focused applications. Future directions include integration with in-browser AI, distributed multi-worker systems, and secure collaboration protocols.
In my view, Web Workers are still underused in React, but they are on track to become a standard tool for performance-driven development.