October 14, 2025

JavaScript runs on a single thread, meaning it can only do one thing at a time. If your app has a heavy task—like sorting a huge list of numbers—it can freeze the user interface (UI), making the app feel slow and unresponsive. This is bad for user experience (UX). To fix this, we can use Web Workers, which let you run heavy tasks in the background on a separate thread, so the main UI stays smooth.
In this guide, we'll explain Web Workers in simple terms, compare them to React's Concurrent Mode, and introduce the useWorker library. This library makes it easy to add multi-threading to your React apps using hooks. We'll walk through an example of sorting a large array, show code snippets, and discuss when to use it, plus its limitations.
I'll paraphrase the original blog in easier language and add extra explanations where things might be confusing. For instance, I'll explain terms like "thread" (think of it as a worker bee handling one job) and why blocking happens (the main thread gets stuck on the heavy task, ignoring user clicks or animations).
A Web Worker is like a helper script that runs separately from your main JavaScript code. It doesn't touch the UI, so it won't freeze your app. Normally, everything in a browser (like button clicks, animations, and calculations) happens on the "main thread." If a task takes too long, the main thread is busy, and the UI lags.

Extra Explanation: Imagine the main thread as a single chef in a kitchen. If the chef is busy chopping a mountain of veggies (heavy task), they can't serve customers (handle UI interactions). A Web Worker is like hiring a second chef in a separate room—they handle the chopping without stopping the service.
Web Workers are great for:
Without a worker, a heavy task blocks the UI. With one, it runs in parallel, keeping things responsive.
React's Concurrent Mode (using features like startTransition) doesn't truly run tasks in parallel. It just prioritizes urgent tasks (like user input) over non-urgent ones (like rendering a big table). Everything still happens on the main thread, but React switches between tasks quickly.
On Concurrent Mode
Using React useWorkers

Extra Explanation: This is like time-sharing on one computer—pausing a slow download to check email. It's helpful but not as powerful as true multi-threading, where tasks run simultaneously on different "cores" (like multiple computers working together).
In contrast, Web Workers use actual separate threads, so a big table render won't interrupt a search bar at all.
The useWorker library is a small (just 3KB) React hook that wraps Web Worker APIs. It makes setup easy—no messy event listeners or complex code. Key features include:
Why use this instead of plain JavaScript Web Workers? Plain ones require more boilerplate code, like creating a separate worker file and handling messages manually. useWorker simplifies it to a single hook.
Here's a quick side-by-side to show the difference. We're sorting an array of numbers.
Plain JavaScript Web Worker: You need a separate file for the worker:
// webworker.js
self.addEventListener("message", function(event) {
var numbers = [...event.data];
postMessage(numbers.sort());
});
Then in your main React file:
// index.js
var webworker = new Worker("./webworker.js");
webworker.postMessage([3, 2, 1]);
webworker.addEventListener("message", function(event) {
console.log("Message from worker:", event.data); // [1, 2, 3]
});
With useWorker: No separate file needed—just a function:
// index.js
const sortNumbers = numbers => ([...numbers].sort());
const [sortWorker] = useWorker(sortNumbers);
const result = await sortWorker([1, 2, 3]);
See? Much cleaner. useWorker handles the worker creation and communication behind the scenes.
Let's build a simple React app that sorts 50,000 random numbers. We'll compare doing it on the main thread (which freezes the UI) vs. using useWorker (which doesn't).
Run this in your terminal:
yarn add @koale/useworker
Or use npm if you prefer:
npm install @koale/useworkerIn your component:
import { useWorker, WORKER_STATUS } from "@koale/useworker";
Extra Explanation: The "kill" function is like hitting stop on a long-running process—useful if the user cancels or it times out.
Here's the full code for a SortingArray.js component. It uses a bubble sort algorithm (simple but slow for big lists, perfect for demoing slowness). We also add toast notifications for feedback.
First, define a bubble sort function in a separate file like algorithms/bubblesort.js:
// bubblesort.js
export default function bubleSort(numbers) {
// Simple bubble sort implementation - swaps adjacent elements if out of order
for (let i = 0; i < numbers.length; i++) {
for (let j = 0; j < numbers.length - i - 1; j++) {
if (numbers[j] > numbers[j + 1]) {
[numbers[j], numbers[j + 1]] = [numbers[j + 1], numbers[j]];
}
}
}
return numbers;
}
Now the component:
import React from "react";
import { useWorker, WORKER_STATUS } from "@koale/useworker";
import { useToasts } from "react-toast-notifications";
import bubleSort from "./algorithms/bublesort";
const numbers = [...Array(50000)].map(() => Math.floor(Math.random() * 1000000));
function SortingArray() {
const { addToast } = useToasts();
const [sortStatus, setSortStatus] = React.useState(false);
const [sortWorker, { status: sortWorkerStatus }] = useWorker(bubleSort);
const onSortClick = () => {
setSortStatus(true);
const result = bubleSort(numbers); // Runs on main thread
setSortStatus(false);
addToast("Finished: Sort", { appearance: "success" });
console.log("Bubble Sort", result);
};
const onWorkerSortClick = async () => {
const result = await sortWorker(numbers); // Runs in worker
console.log("Bubble Sort useWorker()", result);
addToast("Finished: Sort using useWorker.", { appearance: "success" });
};
return (
<div>
<section>
<button disabled={sortStatus} onClick={onSortClick}>
{sortStatus ? "Loading..." : "Bubble Sort (Main Thread)"}
</button>
<button disabled={sortWorkerStatus === WORKER_STATUS.RUNNING} onClick={onWorkerSortClick}>
{sortWorkerStatus === WORKER_STATUS.RUNNING ? "Loading..." : "Bubble Sort useWorker()"}
</button>
</section>
<section>
<span>Open console to see results.</span>
</section>
</div>
);
}
export default SortingArray;
In App.js, add a rotating logo to visualize UI blocking (it stops rotating if the main thread is busy). Install react-toast-notifications for toasts.
import React from "react";
import { ToastProvider } from "react-toast-notifications";
import SortingArray from "./SortingArray";
import logo from "./react.png"; // Your React logo image
import "./style.css"; // Basic styles for rotation
let turn = 0;
function infiniteLoop() {
const logoElement = document.querySelector(".App-logo");
turn += 8;
logoElement.style.transform = `rotate(${turn % 360}deg)`;
}
export default function App() {
React.useEffect(() => {
const loopInterval = setInterval(infiniteLoop, 100);
return () => clearInterval(loopInterval);
}, []);
return (
<ToastProvider>
<div className="App">
<h1>useWorker Demo</h1>
<header>
<img src={logo} className="App-logo" alt="logo" />
</header>
<hr />
<SortingArray />
</div>
</ToastProvider>
);
}
Let’s check the performance profiling of this using the chrome performance recording.

Let’s check the performance profiling of this using the chrome performance recording.

Extra Explanation: Use browser dev tools (like Chrome's Performance tab) to record. Main thread sort shows a long block (e.g., 3-4 seconds). Worker sort keeps the main thread free, with the task in a separate worker thread.
Use them for tasks that eat up CPU and might freeze the UI:
Don't use them for everything—only when needed, to avoid extra complexity.
Extra Explanation: Isolation is for security and performance, but it means you can't directly manipulate the page from a worker. You pass data back and forth via messages.
useWorker makes it simple to add multi-threading to React apps, keeping your UI responsive during heavy work. It's built on Web Workers but easier to use with hooks. Remember, don't overuse workers—save them for tasks that truly need them to keep your code clean.
If you're new to this, experiment with the example code. It can make your apps feel much faster!