🧵 JavaScript Is Single‑Threaded… Until It’s Not
JavaScript is often referred to as a single‑threaded language. But what does that mean?
In simple terms, being single‑threaded means JavaScript can only use one CPU core at a time, typically referred to as the main thread. All your logic, UI rendering, event handling—everything—runs here. It does this using the event loop, which lets JavaScript execute asynchronous operations (like timers or network requests) without completely blocking the thread.
Here’s the catch: just because JavaScript is single‑threaded doesn’t mean everything has to run on the main thread.
🛠️ Tasks That Don’t Run on the Main Thread
Some operations—like setTimeout
, fetch
, or DOM events—are offloaded to the browser’s Web APIs or the system. Once they finish, they queue up a callback to be executed by the event loop.
console.log('Start');
setTimeout(() => console.log('Timeout finished'), 1000);
console.log('End');
Even though setTimeout is called early, its callback is deferred, letting other code keep running. This is concurrent, not parallel.
💡 So… Why Do We Even Need Another Thread?
Fair question. If async code and the event loop are already powerful, why introduce more complexity?
Because some tasks are CPU‑intensive—they need raw computation and can take a long time. Examples include:
- Complex mathematical calculations
- Image or video processing
- Parsing massive data sets
- Matrix multiplication
If you run these directly on the main thread, your UI will freeze. No clicks, no scrolls, no animations—just pain.
Enter the big guns, WEB WORKERS!!!
⚙️ Enter: Web Workers
Web Workers are background scripts that run in a separate thread from the main JavaScript execution thread.
They let you do heavy computation without touching the main thread, so your UI stays smooth and interactive.
🧠 Think of Web Workers as JS-powered background employees. You send them a task, they work on it without bothering the main UI, and when they're done—they report back.
✅ Key Points
- Run on a separate thread
- Can’t access the DOM directly
- Communicate via postMessage / onmessage
- Available in both browsers and Node.js
🧪 Example: Web Worker in Node.js
// worker.js
const { parentPort } = require('worker_threads');
parentPort.on('message', (data) => {
const result = data.a + data.b;
parentPort.postMessage(result);
});
// main.js
const { Worker } = require('worker_threads');
const worker = new Worker('./worker.js');
worker.postMessage({ a: 10, b: 20 });
worker.on('message', (result) => {
console.log('Result from worker:', result);
});
🧮 Browser Demo: Matrix Multiplication in a Web Worker
Let’s build a simple browser demo:
- Input box to enter a number (e.g., 5)
- Submit button
- Toggle: “Use Web Worker” ON/OFF
- On submit:
- Generate two random N×N matrices
- Multiply them inside a Web Worker (if toggle ON) or on the main thread (if toggle OFF)
- Display time taken
👉 React App Structure
/*
File: src/App.jsx
*/
import React, { useState, useRef } from "react";
import MatrixWorker from "./matrixWorker?worker";
const worker = new MatrixWorker();
function App() {
const [size, setSize] = useState(5);
const [useWorker, setUseWorker] = useState(true);
const [timeTaken, setTimeTaken] = useState(null);
const workerRef = useRef(null);
const randomMatrix = (n) =>
Array.from({ length: n }, () =>
Array.from({ length: n }, () => Math.floor(Math.random() * 10))
);
const multiplyMatrices = (A, B) => {
const n = A.length;
const C = Array.from({ length: n }, () => Array(n).fill(0));
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
for (let k = 0; k < n; k++) {
C[i][j] += A[i][k] * B[k][j];
}
}
}
return C;
};
const handleMultiply = () => {
const n = Number(size);
const matA = randomMatrix(n);
const matB = randomMatrix(n);
const start = performance.now();
if (useWorker) {
if (!workerRef.current) {
workerRef.current = new MatrixWorker();
}
workerRef.current.postMessage({ matA, matB });
workerRef.current.onmessage = (e) => {
const duration = (performance.now() - start).toFixed(2);
setTimeTaken(`${duration} ms (Web Worker)`);
};
} else {
multiplyMatrices(matA, matB);
const duration = (performance.now() - start).toFixed(2);
setTimeTaken(`${duration} ms (Main Thread)`);
}
};
return (
<div style={{ padding: "2rem", fontFamily: "sans-serif" }}>
<h1>Matrix Multiplication Demo (React)h1>
<div style={{ marginBottom: "1rem" }}>
<label>
Matrix size:
<input
type="number"
value={size}
min={1}
onChange={(e) => setSize(e.target.value)}
style={{ marginLeft: "0.5rem",width: "4rem" }}
/>
label>
<label style={{ marginLeft: "1rem" }}>
Use Web Worker:
<input
type="checkbox"
checked={useWorker}
onChange={(e) => setUseWorker(e.target.checked)}
style={{ marginLeft: "0.5rem" }}
/>
label>
div>
<button onClick={handleMultiply} style={{ padding: "0.5rem 1rem" }}>
Multiply
button>
{timeTaken && (
<p style={{ marginTop: "1rem" }}>Done in {timeTaken}p>
)}
div>
);
}
export default App;
/*
File: src/matrixWorker.js
*/
self.onmessage = function (e) {
const { matA, matB } = e.data;
const n = matA.length;
const C = Array.from({ length: n }, () => Array(n).fill(0));
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
for (let k = 0; k < n; k++) {
C[i][j] += matA[i][k] * matB[k][j];
}
}
}
postMessage(C);
};
🧪 Try It Yourself
To really feel the difference, try multiplying two 1500 x 1500
matrices using both options:
- ✅ With Web Worker — your UI stays responsive, the "Multiply" button works, and the browser doesn’t freak out.
- ❌ Without Web Worker — you’ll notice the UI freezes up completely until the computation is done.
Why? Because your main thread is busy doing math and has no time to update the UI, handle clicks, or even blink.
This is exactly why Web Workers exist — to offload heavy computation without blocking your app's user experience.
🛠 Other Ways to Offload Heavy Computation
Web Workers aren't the only way to avoid locking up the UI. Here are a few other techniques and tools:
WebAssembly (Wasm)
Compile C, C++, or Rust to run high-performance code in the browser. Great for math-heavy tasks.GPU.js
Leverage your GPU for parallel computations using WebGL. Perfect for things like image processing or large matrix operations.Service Workers
Mostly used for caching and background sync, but can also handle some async logic (not for CPU-heavy stuff though).setTimeout / requestIdleCallback
For breaking up large loops into chunks and letting the UI breathe in between.Backend offloading (API calls)
If the task is too intense for the browser, do it server-side in Python, Go, etc., and just send the result back.
🧠 Bottom line: Don't block the main thread. Ever. Keep it snappy, keep it async, and keep your users happy.
🎉 Conclusion & Next Steps
Web Workers let you offload CPU‑heavy tasks to a separate thread, keeping your UI responsive. Play with the demo, tweak it, and drop it into your next project!
Want to go deeper? Try integrating Web Workers into a real project — like animations, data visualization tools, or AI models running in the browser.
Happy coding—now go make your apps buttery smooth. 🚀