This article is published in English.
JavaScript's Single Thread vs. the Browser's Multi-Process Engine
Explore why JavaScript runs on one thread while browsers juggle networking, rendering, and timers concurrently through the event loop mechanism.
JavaScript runs on a single thread.
You've probably read that statement more times than you can count.
Yet when you load a modern web page, you can see all of the following happening at once:
A network request is in flight.
An image is being fetched and decoded.
A CSS animation is playing smoothly.
The page responds instantly to a click.
Content is being painted to the screen.
And your JavaScript code keeps executing the whole time.
So what's really happening under the hood?
If JavaScript only has one thread, who is handling everything else?
Answering that question reveals one of the most important ideas about how browsers actually operate:
JavaScript and the browser are not the same thing.
JavaScript has a main thread
When developers describe JavaScript as single-threaded, they're referring specifically to how JavaScript code executes.
Your code runs on a single main JavaScript thread.
Take this snippet as an example:
console.log("One");
console.log("Two");
console.log("Three");
Each line runs strictly after the one before it.
JavaScript won't run these three statements at the same moment on the same thread.
There's a single call stack to work with.
Only one chunk of JavaScript executes at any given instant.
That's the essence of being single-threaded in this context.
But this is where it gets more nuanced.
The browser itself is responsible for far more than just running your scripts.
The browser has more jobs than running JavaScript
A browser is a much bigger machine than the JavaScript engine alone.
It has to manage tasks such as:
- Fetching data over the network
- Scheduling timers
- Capturing keyboard and pointer input
- Producing frames on screen
- Decoding images
- Playing sound
- Handling video playback
- Persisting data to disk
- Calculating page layout
- Drawing pixels during paint
- Combining layers during compositing
- A variety of other tasks owned by the browser and operating system
Your JavaScript code isn't the one directly carrying out all of this.
Instead, JavaScript can delegate work to the browser and let it handle the details.
For instance:
fetch("/api/users");
Your script kicks off the request.
But your JavaScript isn't manually opening a socket or shuttling individual bytes from the server itself.
That responsibility belongs to the browser and the systems underneath it.
Once the response comes back, the browser schedules your callback or promise continuation to run on the JavaScript thread.
This is quite different from imagining that:
"JavaScript handles absolutely everything."
Think of JavaScript as one worker inside a much bigger system
Picture a restaurant as a mental model.
JavaScript is a single server taking orders.
The browser represents the entire restaurant operation.
Many other workers are responsible for different tasks behind the scenes.
JavaScript might say:
"I need this data from the server."
At that point, the browser takes over the network operation.
JavaScript doesn't need to freeze and wait until every byte has arrived.
It's free to move on to other work.
Once the operation finishes and is ready to be handled by JavaScript, the corresponding task gets queued up for the JavaScript thread.
This is the foundation of how asynchronous browser APIs work.
So what happens with fetch()?
Take this example:
console.log("Start");
fetch("/api/users")
.then(() => {
console.log("Users received");
});
console.log("End");
Newcomers often picture something along these lines:
Start
↓
fetch()
↓
wait for server
↓
Users received
↓
End
But that's not an accurate picture of what occurs.
JavaScript execution doesn't pause at the fetch() call to wait for the response to come back.
A more accurate picture looks like this:
JavaScript
│
├── Start fetch
│
▼
Browser handles network work
│
│
└───────────────┐
│
JavaScript │
continues │
│
▼ │
console.log("End") │
│
▼
Response becomes available
│
▼
Promise continuation
gets scheduled
│
▼
JavaScript runs it
Given that flow, the console output typically looks like:
Start
End
Users received
The key detail here isn't just that fetch() behaves asynchronously.
What matters most is who is actually doing the waiting.
JavaScript's thread is never blocked while it waits on the network.
The event loop connects the pieces
This is exactly where the event loop comes into play.
You can picture the JavaScript environment as having a location where code executes, plus coordination mechanisms that decide when asynchronous work is allowed to resume.
A simplified version of this system looks like:
Browser
│
┌──────────┼───────────┐
│ │ │
Network Timers User Input
│ │ │
└──────────┼───────────┘
│
▼
Scheduling queues
│
▼
Event Loop
│
▼
Call Stack
│
▼
JavaScript
Keep in mind this diagram is a simplification.
Actual browser internals are considerably more complex, and different engines implement these mechanisms in their own ways.
Still, it captures the essential idea:
Running JavaScript is just one piece of a much larger system.
Timers don't secretly run your code in the background
Take this example:
setTimeout(() => {
console.log("Done");
}, 1000);
A tempting way to picture this is:
"JavaScript spins up a separate thread that counts down one second."
That mental model leads you astray.
It's the browser, not JavaScript itself, that implements timer behavior. Once the specified delay has passed, the callback becomes a candidate for scheduling. Only then does JavaScript actually run it — and only once the main thread is free to take it on.
Which means code like this is a bad idea:
setTimeout(() => {
console.log("Done");
}, 1000);
while (true) {}
Why does this break things?
Once the delay ends, the callback is marked as ready to run, but the main thread is trapped inside an infinite loop and never becomes available. The callback has no way to force its way in and interrupt whatever JavaScript is currently executing.
The browser can absolutely know that:
"The timer has fired."
But knowing that isn't enough — JavaScript still needs an opening to actually execute it:
Main JavaScript thread
while (true) {
// never finishes
}
↓
Timer becomes ready
↓
Callback waits
↓
JavaScript never becomes available
This is one of the core distinctions worth internalizing.
Being asynchronous doesn't mean your callback executes on some other thread.
Why isn't the page constantly frozen, then?
Because JavaScript execution is only one of many things the browser is juggling at any given moment.
There's a catch worth calling out, though.
A lot of work that determines how responsive your page feels — including running your JavaScript and portions of the rendering pipeline — happens on that same main thread.
Which is why code like this will still lock up the page:
const start = performance.now();
while (performance.now() - start < 5000) {
// expensive work
}
For a full five seconds, the main thread is occupied running that loop. Meanwhile, any interaction handling or rendering step that also needs the main thread has to wait its turn.
This is precisely the situation people mean when they say:
"JavaScript is single-threaded."
Concurrency at the browser level, single-threading at the JavaScript level
This is the crucial split to keep in mind.
A browser, as a whole process, can spread different kinds of work across multiple threads. The specifics differ from one browser and operating system to another, but under the hood, modern browsers are built to handle a lot concurrently.
Roughly speaking, work might be distributed like this:
Browser
│
├── JavaScript execution
├── Network activity
├── Rendering-related work
├── Image/media processing
├── Browser services
└── Other internal tasks
However, none of that gives you a way to write something like:
runThisFunctionOnAnotherBrowserThread();
and have arbitrary JavaScript code hop onto some other thread.
Your regular JavaScript still runs according to the single-threaded execution model it always has.
If you specifically need to move heavy computation in JavaScript off the main thread, that's what Web Workers are for.
Conclusion
Single-threaded JavaScript doesn't imply that the browser as a whole is confined to one thread.
Your code executes on a single main thread, while the browser itself takes care of a wide range of other duties through its own internal machinery.
Things like network calls, timers, user input handling, rendering, and media decoding can all proceed independently of your JavaScript's execution.
Once any of that background work is ready to continue, the browser lines up the corresponding callback or promise continuation so your JavaScript can pick it up.
That said, the main thread still carries a lot of weight.
Long-running JavaScript on that thread will delay everything else competing for it — interactions, rendering, and other pending tasks alike.
So you end up with a browser that's quite concurrent overall, paired with JavaScript execution that stays strictly single-threaded.
Grasping that split explains both what browser-based JavaScript is capable of and where its limits lie.
Key takeaways
Hold on to these three points:
- JavaScript itself is single-threaded. Your code executes sequentially, one step at a time, on the main thread.
- The browser is a bigger, concurrent environment. Beyond running your JavaScript, it manages networking, timers, rendering, input, and media handling in parallel.
- Async doesn't mean multithreaded execution of your callback. The browser does the actual waiting, then hands control back to JavaScript once the main thread has room for it.
A helpful way to frame it:
Browser
│
├── JavaScript execution
├── Network activity
├── Timers
├── User input
├── Rendering
├── Media processing
└── Other browser systems
JavaScript is just one component operating inside that larger system, not the system itself.
With that framing in place, ideas like the event loop, asynchronous APIs, UI freezing, and browser-level concurrency become far easier to reason through.