Sixty Times a Second
even more prolly...

Engineering @ Bolt
You type a URL, press enter, and a page appears. A bunch of things happen in succession, and you see your kitty video, but between the moment the first byte of HTML arrives and the moment you see pixels on screen, the browser performs a sequence of operations that would be deeply impressive if it were not so invisible. Maybe the server section that goes across the DNS is known to more than this section. Most engineers who build for the web every day could not fully describe what those operations are.
The browser starts with bytes. The server sends HTML as text. The browser parses it, character by character, building a tree structure called the DOM, the Document Object Model. Each HTML tag becomes a node. The nesting of tags becomes the parent-child relationships in the tree. <html> is the root. <body> is its child. Your <div> is inside that. This is the DOM, a live in-memory representation of the structure of your page.
While parsing HTML, the browser encounters a <link> tag pointing to a CSS file. It fetches it. It then parses the CSS and builds a second tree called the CSSOM, the CSS Object Model. This works the same way, selectors become nodes, specificity and inheritance are resolved, and every element ends up with a computed set of style rules that apply to it.
Now, here is the thing, the browser cannot render anything until it has both the DOM and the CSSOM. CSS is render-blocking. A stylesheet (the SS in CSS) that takes 500ms to download means nothing appears for 500ms. Not a blank version without styles. Nothing. The browser waits. This is why putting your CSS in the <head> and loading it early matters, and why large CSS bundles that contain styles you do not use on the current page are a quiet tax on every page load.
JavaScript is even more aggressive about this. A <script> tag in the <head> without async or defer halts HTML parsing entirely while the script downloads and executes. The browser stops building the DOM. It waits. It executes the script. Then it continues. This is why the old advice was "put scripts at the bottom of the body." The modern advice is defer or async on your script tags, which allows parsing to continue while the script downloads.
Once the browser has the DOM and the CSSOM, it combines them into a third tree called the render tree. The render tree contains only the elements that will actually be displayed. Elements with display: none are excluded entirely, and elements with visibility: hidden are included but marked invisible. The render tree is what the browser will actually draw.
Then comes layout, sometimes called reflow. The browser walks the render tree and calculates the exact position and size of every element on the page. Where does this <div> start? How wide is it? How tall? Where does the text wrap? This is geometry. It is computed in the abstract coordinate system of the page, not pixels on your screen yet. Layout is expensive because elements affect each other, and changing the width of a parent can change the position of every child, which can cascade outward through the tree.
After layout comes paint. The browser fills in the pixels. Background colors, borders, text, images, shadows, all of it gets rasterized into bitmaps. Paint is divided into layers. Some elements, particularly those that move independently of everything else, like a fixed header or an animated element, get their own layer. This is handled by the compositor.
The compositor is the final step. It takes all the painted layers and combines them into the final image you see. Crucially, compositing happens on the GPU, not the CPU. This matters because GPUs are extremely good at combining 2D bitmaps. If an animation only changes the composition of layers rather than triggering layout or paint, it runs entirely on the GPU and stays smooth.
This pipeline runs once on first load. Then your JavaScript changes something, and the pipeline runs again, partially or entirely, depending on what changed. This is where most performance problems come from.
Changing width on an element triggers layout. The browser has to recalculate the geometry of everything affected. Then paint. Then composite. Three phases.
Changing background-color triggers paint but not layout. Two phases.
Changing transform or opacity triggers only compositing. One phase, on the GPU. This is why transform: translateX(100px) is dramatically cheaper than left: 100px, even though they move an element the same distance. One triggers layout. One does not.
The rule is simple, if you can express an animation using only transform and opacity, do it. The browser will keep it on the GPU, skip layout and paint entirely, and hit 60 frames per second without breaking a sweat. The moment you animate width, height, top, left, padding, margin, or anything that affects geometry, you are back to triggering layout on every frame, which means the CPU is working hard on every frame, which is where your jank comes from.
Layout thrashing is the thing that makes this genuinely painful.
// every iteration: read triggers layout, write invalidates it, next read triggers it again
for (let i = 0; i < 100; i++) {
const width = element.offsetWidth // read: forces layout recalculation
element.style.width = (width + 1) + 'px' // write: invalidates layout
}
// batch reads and writes separately
const width = element.offsetWidth // one layout recalculation
for (let i = 0; i < 100; i++) {
element.style.width = (width + i) + 'px' // writes only, no forced recalculation
}
When you read a layout property (offsetWidth, offsetHeight, getBoundingClientRect, scrollTop) after writing to the DOM, the browser is forced to flush any pending layout changes and recalculate before it can give you the current value. If you interleave reads and writes in a loop, you trigger a layout recalculation on every single iteration. The browser knows you are doing this. It does not say anything. It just gets slower and slower until your animation is janky and your users notice.
The fix is to batch all reads before all writes. Read everything you need first. Then write. One layout recalculation instead of a hundred.
The first time you look at a Performance recording and see a hundred purple Layout bars where there should be two, something clicks. You start looking at your CSS differently. You start reading before writing. You stop animating left and start animating transform. This is what your browser is doing for every tab, 60 times a second, across browsers. 60 times. Per. Second.
One small thing before you go. I keep saying sixty times a second. Your 120Hz phone display would disagree.
The browser does not choose its own cadence. It follows a hardware signal called VSync, which your display fires every time it is about to refresh. requestAnimationFrame fires in lockstep with that signal, sixty times per second on a 60Hz monitor, a hundred and twenty times on a 120Hz screen, and so on. The browser just follows the pulse.
Also, the title of this blog is now slightly wrong on half the devices reading it. Such is the nature of writing about hardware on the internet. I stand by it, you know. I usually intend my puns.



