Yo, React devs! I shared a React Web Workers demo with my team, and they loved it—so I’m turning it into a blog post! 🚀
This guide dives into using Web Workers to keep your React apps smooth and responsive, even with heavy computations. We’ll walk through a practical example, tackling a common issue and building up to solid solutions.
Check out the full source code for each step in this GitHub repo!
😑 Problem – A Frozen React UI
Picture a React app tasked with calculating a Fibonacci number, say 42, using a simple recursive function:
// src/App.tsx (part-01-heavy-work-lock-ui)
function fib(n: number): number {
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}
Now, trigger this function on a button click:
// src/App.tsx (part-01-heavy-work-lock-ui)
const handleClick = () => {
setResults((results) => [
...results,
{ id: idRef.current++, result: fib(42) },
]);
};
Why React Web Workers Solve UI Freezing Issues
What happens? 🤔 The UI freezes! Buttons become unresponsive, animations lag, and the app feels dead. Why? JavaScript’s single-threaded nature means the heavy Fibonacci calculation clogs the main thread, which also manages user interactions and rendering.
React Web Workers Demo: Portuguese UI Explained
The UI in this demo is in Portuguese, as it was created for an internal work presentation. I kept it that way since the interface is simple enough to follow, even if you don’t speak Portuguese. 😊
How CSS Animations Work with React Web Workers
Curious why the spinning icon keeps moving while the UI freezes? That’s because CSS animations (like transform: rotate) run on the browser’s compositor thread, not JavaScript, so they stay smooth even when the main thread is blocked.
😋 Solution – Web Workers to the Rescue
To tackle the UI freezing issue in React applications, we can leverage Web Workers to offload intensive computations. Web Workers, a powerful browser API, execute JavaScript in a separate thread, freeing the main thread for smooth user interactions and React UI updates, enhancing React performance and responsiveness.
Here’s how to set up a Web Worker for React:
self.onmessage = ({ data }) => {
const result = fib(data.n);
self.postMessage({ id: data.id, result });
};
function fib(n: number): number {
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}
Then, in your React component, instantiate and communicate with the Web Worker:
useEffect(() => {
workerRef.current = new Worker(
new URL("./workers/fibWorker.ts", import.meta.url),
{ type: "module" }
);
workerRef.current.onmessage = (e: MessageEvent<Result>) => {
setResults((prev) => [...prev, e.data]);
};
}, []);
const handleClick = () => {
if (workerRef.current) {
workerRef.current.postMessage({ id: idRef.current++, n: 42 });
}
};
With this setup, clicking the button triggers the Fibonacci calculation in a background thread via the Web Worker, keeping your React app’s UI responsive and fluid, optimizing React Web Worker performance.
3️⃣ Managing Multiple Tasks in React with Web Workers
When clicking the button rapidly in our React app, each click sends a new message to the Web Worker, which processes them concurrently without guaranteeing the order of completion, potentially disrupting React Web Worker performance.
For example, if we modify our Web Worker to handle asynchronous tasks:
function fib(n: number): number {
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
self.onmessage = async (e: MessageEvent<{ id: number; n: number }>) => {
await delay(Math.random() * 2000);
const result = fib(e.data.n);
self.postMessage({ id: e.data.id, result });
};
This results in unpredictable output order, as shown in the demo.
4️⃣ Ensuring Order with a Queue in React Web Workers
To maintain task order in our React app, we can implement a queue within the Web Worker to process tasks sequentially, enhancing React Web Worker control:
interface Task {
id: number;
n: number;
}
const queue: Task[] = [];
let processing = false;
self.onmessage = (e: MessageEvent<Task>) => {
queue.push(e.data);
if (!processing) processNext();
};
function processNext() {
if (queue.length === 0) {
processing = false;
return;
}
processing = true;
const task = queue.shift();
if (task) {
const result = fib(task.n);
self.postMessage({ id: task.id, result });
}
setTimeout(processNext, 0);
}
This queue ensures tasks are processed one at a time, maintaining order and streamlining React Web Worker workflows.
5️⃣ Using Shared Workers Across Tabs in React
To share a Web Worker across multiple React components or browser tabs, we can use Shared Workers. These allow different contexts (tabs, iframes) from the same origin to access a single worker, perfect for centralized tasks like caching Fibonacci results in React applications.
Here’s a Shared Worker with a cache:
const fibCache = new Map<number, number>();
onconnect = (e: MessageEvent) => {
const port = e.ports[0];
port.start();
port.onmessage = (evt: MessageEvent) => {
// ... (queue logic)
};
};
function fib(n: number): number {
if (fibCache.has(n)) return fibCache.get(n)!;
const result = rawFib(n);
fibCache.set(n, result);
return result;
}
In the React component, connect to the Shared Worker:
useEffect(() => {
sharedWorkerRef.current = new SharedWorker(
new URL("./workers/sharedFibWorker.ts", import.meta.url),
{ type: "module" },
);
sharedWorkerRef.current.port.start();
sharedWorkerRef.current.port.onmessage = (e: MessageEvent<Result>) => {
setResults((prev) => [...prev, e.data]);
};
}, []);
Now, by opening the React app in multiple tabs and calculating the same Fibonacci number, the second tab can retrieve the cached result instantly, thereby boosting React Web Worker efficiency.
Conclusion: That’s it about React Web Workers👍
By integrating React Web Workers and Shared Workers into your React applications, you can significantly enhance performance and user experience, keeping UIs responsive even during computationally intensive tasks.
From offloading tasks to a background thread to managing task order with queues and sharing resources across tabs with caching, these techniques empower React developers to build robust, scalable apps.
Experiment with the provided code in the GitHub repo, and unlock the full potential of React Web Workers to create seamless, high-performance applications!