Multithreading in Javascript
You're probably here because something in your app feels wrong.
A search box lags while results recalculate. A dashboard locks up when it parses a large payload. A Node.js service handles requests well until one CPU-heavy task shows up and everything else slows down. The team starts asking the obvious question: should we use multithreading in JavaScript?
The short answer is yes, sometimes. The more useful answer is that JavaScript concurrency has always been a mix of clever scheduling and strict limits. If you understand those limits, you can decide when workers help, when they hurt, and which newer APIs fit better than the old “just add a Web Worker” advice.
Why Your App Freezes The Single-Threaded Dilemma
JavaScript began with a single-threaded execution model. One call stack runs at a time. That design made early scripting simpler, but it also created a familiar failure mode: one expensive calculation can block everything else.
In the browser, that usually shows up as a frozen UI. A button stops responding. Typing feels delayed. Animations stutter. In Node.js, the symptom is different but the root cause is the same. One CPU-heavy task can block the event loop and delay unrelated work.
That's why multithreading in JavaScript matters. Not because every app needs it, but because some workloads don't fit comfortably inside a single call stack.
What freezing actually means
When developers say “the app froze,” they usually mean the main JavaScript thread got busy and stayed busy. The runtime couldn't return to user events, rendering work, or queued callbacks until that task finished.
Antistatique's overview of multithreading in JavaScript describes the core issue clearly: JavaScript's original model runs one stack at a time, so CPU-heavy work can freeze the UI or block the event loop. The same article notes why browsers introduced Web Workers and why Node.js later added worker_threads for CPU-bound work.
Practical rule: If the problem is “this calculation blocks everything else,” you may need a worker. If the problem is “this request is waiting on the network,” you probably don't.
Why teams reach for the wrong fix
A lot of teams first try to solve CPU pressure with more async code. They split functions, add await, or defer work with timers. Sometimes that improves responsiveness a bit. But async code doesn't make CPU work disappear. It just changes when that work runs.
That distinction matters to engineers, CTOs, and product managers alike. If the bottleneck is computation, the question isn't just “can we make this asynchronous?” It's “can we move this work off the main thread, and is the overhead worth it?”
Concurrency vs Parallelism Understanding the Event Loop
Most confusion around multithreading in JavaScript starts here. People use concurrency and parallelism as if they mean the same thing. They don't.
Concurrency means one system manages multiple tasks by switching between them. Parallelism means multiple tasks run at the same time.

The restaurant analogy that usually clicks
Think of a small restaurant with one chef.
If that chef chops vegetables, checks the oven, plates a dish, and then answers a timer, that's concurrency. One person is handling many tasks by moving between them. It can look busy and efficient, but there's still only one pair of hands doing the work.
If the restaurant hires more chefs and several dishes are cooked at once, that's parallelism. The workload is now distributed across multiple workers.
JavaScript, by default, behaves more like the first restaurant.
The three pieces that matter
You don't need a formal runtime diagram to understand the event loop. You need a usable mental model.
| Runtime part | What it does | Why it matters |
|---|---|---|
| Call stack | Runs the code that's executing right now | Only one stack executes at a time in the main thread |
| Message queue | Holds callbacks and tasks waiting to run | Work can be ready, but still not executing yet |
| Event loop | Checks whether the stack is free, then moves queued work onto it | This is how JavaScript coordinates concurrency |
When you click a button, receive a timer callback, or finish a fetch, JavaScript doesn't magically run everything at once. It places work into queues and runs it when the stack becomes available.
Why async doesn't equal multithreading
Often, mid-level engineers get tripped up.
Promise, setTimeout, and async/await make code non-blocking in structure, but they don't automatically create new CPU execution threads for your JavaScript logic. They help you coordinate waiting. They don't turn one chef into four.
That's why an expensive loop still causes trouble even inside an async function. The code may be wrapped in modern syntax, but once the CPU-heavy part starts on the main thread, the event loop still has to wait for it to finish.
JavaScript feels multitasked because the runtime schedules work cleverly. It becomes truly parallel only when you introduce separate workers.
Where the event loop helps, and where it can't
The event loop is great at workloads with lots of waiting. Network requests, file reads, timers, and user events fit this model well because the runtime can move on while those operations are pending.
It's much worse at raw computation. Parsing a huge dataset, image manipulation, repeated mathematical work, or transforming large in-memory objects can monopolize the thread.
That's the handoff point. Once your bottleneck is compute, not waiting, concurrency alone stops being enough.
Introducing True Parallelism Web Workers and Worker Threads
When JavaScript needs actual parallel execution, it relies on workers.
In browsers, that means Web Workers. In Node.js, that means worker_threads. Both exist for the same reason: CPU-heavy work shouldn't block the main thread that handles UI updates or request coordination.

What a worker actually is
A worker runs JavaScript in the background on a separate thread. Instead of sharing the same execution flow as your main script, it gets its own isolated environment and communicates through messages.
That isolation is the point. Your main thread can stay responsive while the worker handles computation elsewhere.
In practice, the browser side often looks like this:
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ numbers: [1, 2, 3, 4] });
worker.onmessage = (event) => {
console.log('Result from worker:', event.data);
};
// worker.js
self.onmessage = (event) => {
const sum = event.data.numbers.reduce((a, b) => a + b, 0);
self.postMessage(sum);
};
The pattern is simple: send work in, receive results back.
The limitation that surprises people
Workers can't directly manipulate the DOM. That's a feature, not a bug.
If a background thread could mutate the page directly while the main thread was also rendering and handling events, state would become chaotic fast. So the browser draws a firm line. The worker computes. The main thread updates the UI.
That split leads to a healthy architecture:
- Main thread: user input, rendering, DOM updates
- Worker: parsing, transformation, heavy calculations, background processing
Node.js follows the same idea
Node.js added worker_threads for the same class of problem: CPU-bound work. The same Antistatique article notes that Node's documentation positions workers as useful for CPU-intensive operations, but not especially helpful for I/O-bound tasks, because Node's built-in async I/O already handles those efficiently.
That distinction saves teams a lot of wasted effort. If your service spends most of its time waiting on a database, adding worker threads probably adds complexity without solving the primary bottleneck.
For a deeper Node-focused implementation guide, this walkthrough on multithreading in Node.js is useful once you're ready to wire workers into backend code.
Why this matters to hiring and architecture
Teams building CPU-heavy JavaScript systems often underestimate how specialized this work becomes. Designing worker boundaries, message formats, and failure handling is different from routine async application code. If you're staffing for that kind of backend work, it helps to find remote Node.js roles and compare the kinds of concurrency expectations employers now attach to senior Node.js positions.
Workers are not “faster JavaScript” by default. They're a way to move the right work to the right execution context.
From Theory to Practice Code Examples and Performance Gains
A worker is easiest to understand when you feel the pain first.
Say you have a browser app that performs a heavy calculation when the user clicks a button. The code works, but the page becomes unresponsive while that calculation runs.

A blocking browser example
// main.js
const button = document.querySelector('#start');
const status = document.querySelector('#status');
function heavyWork(limit) {
let total = 0;
for (let i = 0; i < limit; i++) {
total += Math.sqrt(i);
}
return total;
}
button.addEventListener('click', () => {
status.textContent = 'Working...';
const result = heavyWork(500000000);
status.textContent = `Done: ${result}`;
});
Nothing about this code is incorrect. The problem is where it runs. The loop occupies the main thread, so the browser can't keep the interface responsive while the work is in progress.
The same idea with a Web Worker
// main.js
const button = document.querySelector('#start');
const status = document.querySelector('#status');
const worker = new Worker('worker.js');
worker.onmessage = (event) => {
status.textContent = `Done: ${event.data}`;
};
button.addEventListener('click', () => {
status.textContent = 'Working...';
worker.postMessage(500000000);
});
// worker.js
self.onmessage = (event) => {
let total = 0;
for (let i = 0; i < event.data; i++) {
total += Math.sqrt(i);
}
self.postMessage(total);
};
The total work may still be expensive, but the user can now scroll, click, and interact while it runs. That's often the first meaningful win. Not “the job finished sooner,” but “the app remained usable.”
If your bottleneck is on the frontend, this is the same kind of architectural thinking behind improving app performance. You first identify whether the user is paying for rendering, waiting, or CPU saturation, then move the right work off the critical path.
A Node.js worker example
In Node.js, the ergonomics are similar:
// main.js
const { Worker } = require('worker_threads');
function runWorker(data) {
return new Promise((resolve, reject) => {
const worker = new Worker('./worker.js', { workerData: data });
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) reject(new Error(`Worker stopped with code ${code}`));
});
});
}
runWorker(500000000)
.then((result) => console.log('Result:', result))
.catch((err) => console.error(err));
// worker.js
const { parentPort, workerData } = require('worker_threads');
let total = 0;
for (let i = 0; i < workerData; i++) {
total += Math.sqrt(i);
}
parentPort.postMessage(total);
This doesn't make every Node.js service better. It makes CPU-heavy sections stop blocking the main event loop.
What real scaling looked like in one published demo
One published Node.js Worker Threads demo made the impact concrete. The job took about 20 seconds with 2 workers, 10 seconds with 4 workers, and 4.7 seconds with 16 workers, while CPU utilization rose from roughly 15% to 30% and then to 100% as concurrency increased, according to the published demo on YouTube. That same demo reported a near 10× speedup versus the original single-threaded run.
Those numbers matter for two reasons.
First, they show that JavaScript can benefit from real parallel CPU execution when the workload is a good fit. Second, they remind you that workers are an operational tool, not just a language feature. You're deciding how aggressively to use available CPU resources.
What developers often miss in benchmarking
A worker benchmark should answer at least three questions:
- Did total job time improve?
- Did the main thread become more responsive?
- Did messaging overhead eat part of the gain?
If you only measure one of those, you can make the wrong call. Some workloads don't need faster completion as much as they need a responsive UI. Others need raw throughput. Some need both.
Benchmark the real task, not a toy loop. Serialization cost, setup time, and result handling can change the outcome.
Beyond Message Passing Shared Memory and Modern APIs
Basic workers communicate by passing messages. That's safe and easy to reason about, but it can become expensive when threads exchange large or frequent data structures.
That's where shared memory enters the picture.
SharedArrayBuffer as a shared whiteboard
A useful analogy is a team sharing one whiteboard instead of photocopying notes back and forth.
With SharedArrayBuffer, multiple threads can access the same block of memory. Instead of cloning a large dataset and sending copies through postMessage, threads can read and update shared data directly through typed array views.
That can remove a lot of transfer overhead, but it introduces a new problem: coordination. If two threads update the same memory at the same time, the result can become unpredictable.
Atomics are the rules for the whiteboard
That's what Atomics are for.
Think of Atomics as the rules that keep people from writing over each other's work on that shared whiteboard. They let you perform reads, writes, and synchronization steps safely enough for shared memory use cases.
If message passing is like mailing sealed envelopes between rooms, shared memory is like giving both rooms access to the same notebook. Faster, yes. Also easier to misuse.
Here's the mentoring advice I give teams: don't start with shared memory because it sounds advanced. Start with message passing. Move to shared memory only when profiling tells you that copying or serialization is a real cost.
Modern concurrency is broader than workers
A lot of articles stop at Web Workers and timers. The ecosystem has moved on.
Honeybadger's overview of JavaScript multithreading points to newer APIs such as OffscreenCanvas, which allows canvas rendering to move off the main thread and helps keep the UI responsive. That's important because sometimes your problem isn't “general computation.” It's a very specific workload like drawing, rendering, or frame-sensitive interaction.
That same overview also notes that newer tools are emerging to make threading easier across Node.js, Deno, and browsers. The interesting shift isn't just more ways to create threads. It's more workload-specific abstractions.
When this matters in production
If you're reaching for shared memory or rendering off the main thread, keep one eye on operational hygiene. Concurrency bugs often masquerade as memory growth, resource retention, or lifecycles that never complete. This guide on how to check for memory leaks becomes relevant fast once threads and long-lived buffers enter the system.
A practical hierarchy usually looks like this:
- Start with async I/O when the work is mostly waiting
- Use workers when CPU work blocks the main thread
- Use shared memory and Atomics when copying data becomes the bottleneck
- Use specialized APIs like OffscreenCanvas when the workload has a purpose-built concurrency path
That last point is the modern one. Don't ask only, “Can we use multithreading in JavaScript?” Ask, “Which concurrency primitive best matches this exact workload?”
The Strategic Decision When to Use Multithreading
The approach taken either saves teams months of engineering time or creates a maintenance burden they didn't need.
Multithreading in JavaScript is powerful. It's also easy to overuse. The hardest part isn't writing new Worker(...). It's deciding whether the problem deserves that complexity.

Where the trade-off shows up
Workers introduce costs:
- Code complexity: you now manage boundaries between threads
- Debugging friction: failures may happen outside the main execution path
- Data movement overhead: some values must be copied or serialized
- Isolation constraints: browser workers can't directly touch the DOM
Those costs are not theoretical. A Hacker News discussion about a JavaScript multithreading library noted diminishing returns in a real build-system workload because serializing and deserializing ASTs created enough overhead to erase the gains. That's the contrarian signal many teams miss. Parallelism can lose to messaging cost.
A decision framework for CTOs and product teams
Before approving worker-based architecture, ask these questions:
Is the pain actually CPU-bound
If the issue is network latency, database response time, or file waiting, workers probably aren't your answer. JavaScript already handles I/O concurrency well with its existing async model.
If the issue is repeated computation, transformation, parsing, rendering, or compression-like work, workers become a stronger candidate.
Does the task run long enough to justify the overhead
Thread setup, communication, and result handling all cost something. A tiny task often finishes before a worker can pay for itself.
Longer-running or repeated CPU tasks tend to justify the extra machinery more often than quick one-off functions.
Can the work be isolated cleanly
The best worker workloads have clear inputs and outputs. You hand over data, the worker computes, and it sends back a result.
The worst candidates depend on frequent back-and-forth chatter, shared mutable state, or direct UI access. Those designs often collapse under coordination cost.
What matters more, throughput or responsiveness
Sometimes the business goal is faster total processing. Sometimes it's a smoother user experience. Sometimes it's protecting a Node.js server from one expensive task.
Those are different goals, and they may lead to different designs. A worker can be worth it even when total task time changes only modestly, if the main thread stays responsive.
If the team can't describe the worker boundary in one sentence, the design probably isn't ready.
Good fits and poor fits
A short comparison helps.
| Likely good fit | Usually poor fit |
|---|---|
| Data transformation on large in-memory datasets | Standard API calls |
| Expensive calculations | Database queries |
| Image or canvas-related background work | Typical CRUD handlers |
| Repeated CPU-heavy backend jobs | Small, short-lived operations |
Rendering work that can move to OffscreenCanvas |
Tasks that need constant DOM access |
The business lens
For product managers and CTOs, the question isn't “Is multithreading modern?” It's “Will it improve user experience or system capacity enough to outweigh added complexity?”
Sometimes the honest answer is no. A simpler optimization, better batching, lighter parsing, or smarter caching can deliver more value with less risk.
That's the mature position. Use multithreading in JavaScript when the workload demands it, not when the architecture diagram looks impressive.
If your team is weighing worker threads, browser performance bottlenecks, or a broader app architecture decision, Nerdify can help you design and build the right solution without overengineering it.