Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

How the Browser Turns HTML Into Pixels (and Why Your Page Still Feels Slow)

About Post

Your API answered in 80 milliseconds. The page still feels slow. The scroll stutters, the table jumps when the fonts load, and the button takes a beat to react.

That's because the server sending HTML is only the start. Between "bytes arrived" and "pixels on screen", the browser runs a whole production line, and most front-end performance problems are about making that line do more work than it needs to. Let's walk it, station by station.

The production line at a glance

  1. Parse HTML into the DOM.
  2. Parse CSS into the CSSOM.
  3. Combine them into the render tree.
  4. Layout: work out the size and position of every box.
  5. Paint: turn boxes into drawing instructions and pixels.
  6. Composite: stack the painted layers together, usually on the GPU.

Every frame you see went through some or all of these steps. The trick to a fast page is understanding which changes send the browser back to which step.

1. HTML becomes the DOM

The browser reads HTML as it streams in and builds the DOM, a tree of nodes. It doesn't wait for the whole file, which is why a fast first byte matters.

The thing that stops this streaming is a plain <script src="...">. The parser can't know whether that script will call document.write() or change the DOM, so it stops, downloads the script, runs it, and only then continues. One slow script in the <head> and the whole page waits.

<!-- Blocks parsing until downloaded and executed -->
<script src="/js/app.js"></script>

<!-- Downloads in parallel, runs after parsing, in order -->
<script src="/js/app.js" defer></script>

<!-- Downloads in parallel, runs as soon as it's ready, any order -->
<script src="/js/analytics.js" async></script>

Use defer for your app code and async for independent things like analytics. Module scripts (type="module") are deferred by default.

2. CSS becomes the CSSOM

Stylesheets are parsed into the CSSOM: every rule, resolved into the final computed style for each element. CSS is render-blocking. The browser won't paint anything until it has the CSS, because painting unstyled content and then restyling it would look broken.

That's a sensible default, and it's why a huge stylesheet from a slow CDN delays the very first paint, even when the HTML arrived instantly.

3. The render tree

The browser walks the DOM, applies the CSSOM and keeps only what will actually be drawn. An element with display: none isn't in the render tree at all. An element with visibility: hidden is: it's invisible, but it still takes up space. That small difference matters later, because only the render tree goes through layout.

4. Layout: the expensive maths

Layout (Firefox calls it reflow) calculates geometry: how wide is this box, where does this line wrap, how tall is this table row. It's expensive because geometry is connected. Make one element taller and everything below it moves. Change the width of a container and every child may need recalculating.

Things that trigger layout: changing width, height, padding, margin, top/left, font size, adding or removing elements, and even reading geometry at the wrong moment. More on that in a second.

5. Paint and 6. composite

Paint fills in the pixels: text, colours, borders, shadows, images. The browser may split the page into layers, paint them separately, and then compositing stacks them together like sheets of glass.

Compositing is the cheap step, often handled by the GPU. That's the key to smooth animation: if you only change things the compositor can handle on its own, the browser skips layout and paint completely.

You change...Browser redoesCost
width, height, top, left, font-sizeLayout, paint, compositeHigh
color, background-color, box-shadowPaint, compositeMedium
transform, opacityComposite only (usually)Low

So animate a sliding panel with transform: translateX(), not by changing left. Same visual result, very different amount of work. At 60 frames per second, the browser has about 16 milliseconds per frame for everything, your JavaScript included.

The classic trap: layout thrashing

Browsers are lazy in a good way. When you change a style, they don't recalculate layout immediately; they batch the work until the next frame. But if you read a geometry value like offsetHeight right after a write, the browser has to calculate layout right now to give you a correct answer.

Do that in a loop and you force a full layout on every iteration:

// Bad: read, write, read, write... a forced layout every time
rows.forEach((row) => {
  const height = row.offsetHeight;       // read (forces layout)
  row.style.height = `${height + 10}px`; // write (invalidates layout)
});

// Better: all reads first, then all writes
const heights = rows.map((row) => row.offsetHeight);
rows.forEach((row, i) => {
  row.style.height = `${heights[i] + 10}px`;
});

On a staff dashboard with a long table of contracts or invoices, the first version is the difference between instant and visibly sluggish. The fix isn't clever. It's just batching.

Practical tips that come straight from the pipeline

  • Don't block the parser: defer your scripts, keep the <head> lean.
  • Get CSS in fast: keep the critical CSS small; load the rest without blocking if it's not needed for the first screen.
  • Reserve space for images: set width and height attributes (or aspect-ratio) so the layout doesn't jump when they load.
  • Animate transform and opacity, not position and size.
  • Batch DOM reads and writes, and do visual updates inside requestAnimationFrame.
  • Use will-change sparingly. It promotes an element to its own layer, which costs memory. Use it on the thing you're about to animate, not on everything.
  • Render less. A table with thousands of DOM rows will always be slow to lay out. Paginate or virtualise the list.

The rule to remember: layout is expensive, paint is medium, composite is cheap. Every performance trick on this list is a way of staying as far down that line as possible.

See it for yourself

Open Chrome DevTools, go to the Performance panel, record yourself scrolling a heavy page, and look for long purple blocks (layout and style) and green ones (paint). Firefox has the same idea in its profiler. Once you've seen a forced layout warning in your own code, you never unsee it.

Which one of these has bitten you: a render-blocking script, a jumping layout, or a scroll that stutters for no obvious reason?

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close