Rendering Is a Browser Decision, Not a JavaScript One
Source: Dev.to
Overview
This is the fifth article in a series on how JavaScript actually runs. You can read the full series here or on my website.
You change the DOM.
You expect the screen to update.
It doesn’t.
Why?
In the earlier articles we established three constraints:
- JavaScript runs to completion.
- Tasks form scheduling boundaries.
- Microtasks must fully drain before moving on.
Now we add a fourth:
The browser will not render while a macrotask is running nor while microtasks are draining.
Rendering Is a Browser Decision
Up to this point we’ve focused on two pieces of the system:
- The JavaScript engine – executes code and manages the call stack.
- The runtime – provides the event loop and scheduling rules.
Neither of these is responsible for rendering. Beyond them, the browser also contains a rendering engine – the subsystem that performs layout and painting.
- The engine executes your code.
- The runtime decides when that code runs.
- The rendering engine decides when the result becomes visible.
For simplicity, this article will refer to that rendering engine simply as the browser.
The Rendering Misconception
When I first started learning JavaScript I carried several mental models that felt reasonable:
- DOM updates render immediately.
- If I change the UI, the user will see it right away.
- The browser renders continuously at 60 fps.
These feel natural because the screen often updates quickly, but they’re incomplete. Rendering does not happen whenever the DOM changes. Instead, it occurs only when there is a “safe opportunity” – after the current macrotask finishes and the microtask queue is empty.
Rendering is not triggered by DOM mutation; it is gated by scheduling boundaries. Let’s test that.
Running the Experiments
These experiments rely on the browser’s rendering behaviour.
Create a simple HTML file with the following content:
Initial Enter fullscreen mode Exit fullscreen modeOpen the file in your browser.
You can run all code snippets in this series by pasting them into the browser console.
Note: These examples will not work in Node.js because they depend on the DOM and browser rendering.
Test 1: DOM Updates Inside One Macrotask
What happens when we have multiple DOM updates within the same macrotask? Consider this code, which uses a placeholder before the final string is ready:
const box = document.getElementById("box");
box.textContent = "Temporary string";
for (let i = 0; i {
box.textContent = "Final string of Test 2";
});Again, we only see “Final string of Test 2”.
The initial macrotask runs and sets “Temporary string.” After the call stack empties, the microtask runs immediately and updates the DOM to “Final string.” Only now does the browser get an opportunity to render.
Microtasks delay rendering just like synchronous code does.
Test 3: Breaking Into a New Task Allows Paint
Now consider a timer callback:
const box = document.getElementById("box");
box.textContent = "Temporary string";
setTimeout(() => {
box.textContent = "Final string of Test 3";
}, 1000);This time we may see “Temporary string,” followed by “Final string of Test 3” a second later.
We have introduced a task boundary. The browser finishes the initial macrotask, drains microtasks (there are none here), and then gets an opportunity to render. If it chooses to render, “Temporary string” becomes visible. Later, when the timer’s macrotask runs, the DOM updates to “Final string,” and the next render reflects this.
Rendering is allowed at task boundaries. This does not mean that rendering is guaranteed between macrotasks; only that it can happen there.
Why Rendering Waits
If the browser could render in the middle of a macrotask or while microtasks are draining, it could display a half‑updated DOM, inconsistent layout, or partially computed state.
With the constraint that rendering only occurs after a macrotask finishes and the microtask queue is empty, the browser renders only stable states. No partial work is in progress, so rendering is atomic with respect to JavaScript execution.
The Correct Mental Model
From the experiments we’ve shown that the browser does not render whenever the DOM changes. Instead:
The browser renders only after JavaScript finishes its turn.
A “turn” means:
- The current macrotask completes, and
- The microtask queue has been fully drained.
Rendering is allowed only at those boundaries. This does not mean the browser renders after every turn; it merely cannot render during a turn. The rendering decision is gated by the same scheduling rules we’ve been building throughout the series.
This Series
What This Prepares Us For Next
If rendering only happens at specific boundaries, a new question emerges: How do we write code that runs at the right moment?
setTimeout creates a new macrotask, but it does not align with the browser’s frame timing.
Microtasks delay rendering, but they do not schedule it. If we want smooth animation and responsive updates, we need a way to run code just before the browser renders the next frame.
This is what requestAnimationFrame is designed for. In the next article, we’ll look more closely at how the browser’s rendering cycle works and how to schedule work in harmony with it.
This article was originally published on my website.