Skip to content
Episode 14 · The page that stutters on Android

The DOM and the render pipeline: from HTML to pixels

Tuesday, 14:20: on a mid-range Android phone, StayHub's search results scroll at 11 frames per second. On Karim's laptop the same page is perfectly smooth, and Maya says 60% of searches come from phones.

How a browser turns text files into the page you see, and why some changes are free while others make the page stutter.

30 min foundationDOM and CSSOM: the browser's models of a pageThe render pipeline: style, layout, paint, compositeReflow vs repaint vs composite-only changesThe JavaScript event loopWhat the frontend is responsible for
  1. Problem

    A hotel search page must show results quickly and stay smooth while the guest scrolls, filters and opens photo galleries on a mid-range phone.

  2. Why the simple system fails

    Treating the page as a static document: render-blocking scripts delay first paint, and JavaScript that changes layout in a loop forces the browser to recompute the whole page dozens of times per frame.

  3. Engineering concept

    The browser builds models (DOM, CSSOM), combines them into a render tree, then runs layout, paint and composite. Each change re-runs part of that pipeline; cheaper changes skip stages.

  4. Technology

    HTML, CSS, JavaScript, the DOM API, defer/async scripts, CSS transforms and opacity, requestAnimationFrame, browser DevTools performance panels.

  5. Trade-offs

    Rich, dynamic UIs need DOM changes, and every change costs some pipeline work. You trade developer convenience (re-render everything) against frame budget (touch only what changed, in the cheapest stage).

01

The problem

2 min
Tuesday, 14:20. Maya holds up her phone.
  1. Maya · Product manager
    Search results stutter when I scroll: 11 frames per second. 60% of searches are on phones like this.
  2. Karim · CTO
    Perfectly smooth on my laptop. Same page.
  3. Lina · Senior engineer, your mentor
    Guess first. Then we look at what the browser does in every frame.
02

The big idea

4 min
Why the phone drops frames
The big idea

Every change re-runs part of the render pipeline. Make changes start as late in it as possible.

It isn't the phone. It's how often our code makes the browser redo layout. The laptop does the same wasted work, just fast enough to hide it.

03

How it works

5 min
From files to pixels
  1. 1
    HTML → DOM

    The browser parses HTML into the , a tree of element objects. JavaScript changes the page by changing this tree.

  2. 2
    CSS → CSSOM

    Style rules are parsed into the .

  3. 3
    Render tree

    Visible elements plus computed styles form the . display: none is left out.

  4. 4
    Layout

    Layout works out every box's size and position. Redoing it is a : change a size and neighbours may move.

  5. 5
    Paint

    Text, colours and images are drawn into layers: a when redone.

  6. 6
    Composite

    Layers are stacked into the frame, often on the GPU ().

All of this shares the main thread with our JavaScript, within a frame budget of 16.7 ms at 60 fps. A change re-runs the pipeline from the earliest stage it affects: width or top → layout onwards (expensive); color → paint onwards; transform or opacity → often composite only (cheap).

Our scroll handler reads each card's position, then writes a new one, for ~25 cards. Each read after a write forces a layout right now: . About 90 ms per frame: 11 fps.

04

Watch a decision being made

4 min
Worked example

Lina fixes the stuttering search page

Search results scroll at 11 fps on a mid-range phone and smoothly on a laptop. Find out why and fix it, without guessing.

  1. 1Reproduce on a slow CPU, find the biggest slice

    DevTools performance panel, CPU slowed 4×, record a scroll. One frame: ~90 ms, layout 80 ms, many small "Layout" blocks inside our handler.

    Why: On a fast laptop the problem hides. Many layouts in one handler is the signature of forced layouts.

  2. 2Batch reads, then writes

    Read all positions first, then do all writes in requestAnimationFrame, so layout runs once per frame.

    ts
    const ys = visibleCards.map((c) => c.offsetTop);   // all reads
    requestAnimationFrame(() => {
      visibleCards.forEach((c, i) => {
        c.style.transform = `translateY(${ys[i] + shift}px)`;  // all writes
      });
    });

    Why: Reads are cheap when nothing changed since the last layout. Grouping removes 24 of the 25 layouts.

  3. 3Move with transform

    Write transform instead of top; make the sticky price bar position: sticky instead of moving it in JavaScript.

    Why: A transform changes neither geometry nor pixels, so the browser can skip layout and paint.

  4. 4Measure again on the phone

    With the CPU slowed 4×: about 6 ms per frame, 60 fps. Then confirm on a real mid-range phone.

    Why: A fix you haven't measured is a hope.

The whole thing in one line: Read, then write. Move with transform. Measure on the device guests actually hold: ~90 ms became ~6 ms per frame.
05

In real life

3 min
How much work to give the DOM

Figma: draw on a canvas, skip the DOM

  • Has publicly described rendering its design canvas itself, with WebGL
  • Thousands of shapes move at once; a DOM node each would mean constant layout
  • Text, selection and accessibility must be rebuilt by hand

Worth it for a drawing surface with huge numbers of moving objects.

Wikipedia: plain HTML, small DOM

  • Articles are mostly server-built HTML and CSS, little JavaScript
  • Fast on old phones; text, links and screen readers come free

Worth it when the page is mostly content to read.

06

What goes wrong

3 min
Animating top, left, width or height

Each frame triggers layout and paint; slow devices drop frames.

Instead

Animate transform and opacity.

Thinking a framework removes rendering costs

React decides which DOM changes to make; the browser still runs layout and paint for each.

Instead

Keep DOM changes per interaction, and the DOM itself, small.

Images without dimensions

When the image loads, the browser reflows and content jumps.

Instead

Set width/height or aspect-ratio.

07

Trade-offs

3 min
OptionFits whenYou payMain risk
Re-render big parts on each changeSmall pages, internal toolsLayout work per interactionJanky on slow phones
Targeted, minimal updatesLarge lists, frequent updatesMore code and bookkeepingStale UI bugs
Transform/opacity animationsAnything that moves or fadesGPU memory per layerNot every effect fits
Friday, 10:00. Maya wants filters (pool, breakfast, price).
  1. Karim · CTO
    On every filter change, throw away the list and rebuild all 200 cards. Ship today.
  2. Lina · Senior engineer, your mentor
    On Maya's phone that's about 200 ms per tap. Taps will feel ignored.
  3. Karim · CTO
    Updating only changed cards means more code and more ways to show stale cards.
08

Your job as tech lead

4 min

Claude Code can write the code for this. You still own the decisions, and you have to know what good looks like to accept or reject its work.

Decisions you own
  • Which device you judge smoothness on: a mid-range Android phone, with 60 fps (16.7 ms per frame) as the bar.
  • Which third-party scripts may sit on the page, and whether they may block first paint.
  • Which animations are worth having, and that they move things with transform or opacity.
Tell Claude Code
On a mid-range Android phone, StayHub's search results (200 cards) scroll at about 11 fps: ~90 ms per frame, ~80 ms layout. The scroll handler reads offsetTop and writes style.top for ~25 visible cards. Stay under the 16.7 ms budget: no position reads after style writes in one loop, move the sticky bar with transform, use IntersectionObserver for visibility. Don't change the search API or card design. Plan first, then traces with 4× CPU slowdown before and after, with time per frame.
Check before you accept
  • In the traces, "Layout" shrinks from ~80 ms to a few ms per frame.
  • No loop both reads a position (offsetTop, getBoundingClientRect) and writes a style.
  • Any new <script> in <head> has defer or async.
Red flags
  • It animates top, left or width for something that moves every frame.
  • It throttles the scroll handler so the page updates late instead of smoothly.
  • It judges speed only on a fast laptop, or measures nothing.
09

Check yourself

2 min
Q1/3Debugging
The DevTools performance panel shows dozens of purple "Layout" blocks inside one scroll handler call, totalling 80 ms. What is the likely pattern in the code?
window.addEventListener("scroll", () => {
  for (const card of cards) {
    if (card.getBoundingClientRect().top < window.innerHeight) {
      card.style.opacity = "1";
      card.style.marginTop = "0px";
    }
  }
});
How confident are you?

Next episode: Google shows StayHub's hotel pages with no description, and the phone waits for 300 KB of JavaScript before showing a single hotel. Who should build the HTML?

Finished?

Mark it complete to update your skill map.

Go deeper (optional)You don't need this to understand the lesson. Open it when a project needs the details: Technical deep dive, Visual diagram, Analogy, Check your understanding, Practice ladder, Explain it yourself.
10

Technical deep dive

From 90 ms to 6 ms per frame

Two pieces of code decide most of what happens: how the page loads its scripts, and how JavaScript changes the DOM afterwards.

html
<head>
  <!-- Render-blocking by design: the browser waits for CSS before first paint. Keep it lean. -->
  <link rel="stylesheet" href="/app.css" />
  <!-- defer: download in parallel, run after the HTML is parsed, in order. -->
  <script src="/app.js" defer></script>
</head>
<body>
  <ul id="results">
    <li class="card"><img src="/h/42.jpg" width="320" height="200" alt="Hotel Alfama" /> Hotel Alfama · €140</li>
  </ul>
</body>
Explicit image dimensions let layout reserve space before the photo arrives.
Worked example

Lina fixes the stuttering search page

The search results page scrolls at 11 fps on a mid-range Android phone and smoothly on a laptop. Find out why and fix it, without guessing.

  1. 1Reproduce on a slow CPU

    Open DevTools' performance panel, slow the CPU down 4×, scroll the results for 3 seconds and record.

    Why: On a fast laptop the problem hides. You can only fix what you can see.

  2. 2Find the biggest slice

    One scroll frame takes about 90 ms: layout 80 ms, scripting 4, paint 5, composite 1. Many small purple "Layout" blocks sit inside our scroll handler.

    Why: Many layouts inside one handler is the signature of forced layouts, not of a big page.

  3. 3Name the cause

    The handler reads offsetTop then writes style.top, card after card. Each read after a write forces a synchronous layout: about 25 per event.

    ts
    for (const card of visibleCards) {
      const y = card.offsetTop;           // READ  → forces layout
      card.style.top = y + shift + "px";  // WRITE → invalidates layout
    }

    Why: The cause picks the fix. A smaller page wouldn't help much; stopping the read-write ping-pong does.

  4. 4Batch reads, then writes

    Read all positions first, then do all writes inside requestAnimationFrame, so layout runs once per frame.

    ts
    const ys = visibleCards.map((c) => c.offsetTop);   // all reads
    requestAnimationFrame(() => {
      visibleCards.forEach((c, i) => {
        c.style.transform = `translateY(${ys[i] + shift}px)`;  // all writes
      });
    });

    Why: Reads are cheap when nothing has changed since the last layout. Grouping them removes 24 of the 25 layouts.

  5. 5Move with transform

    Write transform instead of top, and make the sticky price bar position: sticky instead of moving it in JavaScript.

    Why: A transform changes neither geometry nor pixels, so the browser can skip layout and paint and only composite.

  6. 6Measure again on the phone

    New recording with the CPU slowed 4×: about 6 ms per frame, 60 fps. Then check on a real mid-range phone.

    Why: A fix you haven't measured is a hope. It also shows the next bottleneck, if any.

The whole thing in one line: Lina reproduced the stutter on a throttled CPU, found that forced layouts were 80 of the 90 ms frame, batched reads before writes and moved things with transform: one frame went from ~90 ms to ~6 ms.
ChangeExample propertiesStages re-runRelative cost
Geometrywidth, height, margin, top, font-size, adding/removing nodesStyle → Layout → Paint → CompositeHigh
Appearance onlycolor, background, box-shadow, visibilityStyle → Paint → CompositeMedium
Compositor-onlytransform, opacity (on its own layer)CompositeLow
Reading geometry after a writeoffsetHeight, getBoundingClientRect, scrollTopForces a layout nowHigh inside loops
Exact behaviour varies by browser; the performance panel shows what actually ran.
11

Visual diagram

Click a component

The browser's render pipeline for a hotel search page. Start the walkthrough.

12

Analogy

  • Making a magazineThe editor's outline of the articlesRendering a pageThe DOM
  • Making a magazineThe style guide, filed and indexedRendering a pageThe CSSOM
  • Making a magazineA designer placing every blockRendering a pageLayout (reflow when redone)
  • Making a magazineInking each blockRendering a pagePaint (repaint when redone)
  • Making a magazineSliding a transparent sheet sidewaysRendering a pageCompositing: cheap transform moves
  • Making a magazineAsking page height after every editRendering a pageLayout thrashing

Where the analogy breaks: a magazine is laid out once. A browser may redo layout and paint dozens of times per second while you scroll, so small costs add up fast.

13

Check your understanding

Q1/3Recall
Put the browser's first render in order:
  1. 1.Composite layers into the frame on screen
  2. 2.Combine them into the render tree (computed styles)
  3. 3.Paint: draw pixels into layers
  4. 4.Parse HTML into the DOM and CSS into the CSSOM
  5. 5.Layout: compute size and position of every box
How confident are you?
14

Practice ladder

Step 1 · Trace

Which stages does each change run?

Scenario Warm-up

When a guest taps "Show map", StayHub's code does four things:

filterDrawer.style.transform = "translateX(-100%)"; // 1. slide the drawer out
mapButton.style.backgroundColor = "#0a6";           // 2. highlight the button
mapPanel.style.height = "400px";                    // 3. open the map panel
const h = resultsList.offsetHeight;                 // 4. read the list's height

For each line, say which pipeline stages the browser has to run (layout, paint, composite), and explain what's special about line 4.

Step 2 · Complete

Finish Lina's note on the photo gallery

Debugging Practice

Maya: "Swiping through hotel photos is jerky on my phone." The gallery code:

function showPhoto(i: number) {
  for (const img of photos) {
    img.style.left = (img.offsetLeft - i * 360) + "px";
  }
}

The performance panel shows 12 layouts per swipe, 70 ms in total. Lina's note has two gaps. Fill in steps 3 and 5.

  1. Reproduce on a slow CPU: done, 70 ms per swipe frame.
  2. Find the biggest slice: layout, 12 times per swipe.
  3. Name the cause: ___
  4. Batch reads, then writes.
  5. Change where the work starts: ___
  6. Measure again.
Step 3 · Solve

The checkout page shows a white screen

Debugging Practice

Maya: "On slow 4G, checkout shows a white screen for about 2.5 s, then everything appears at once." The checkout page's <head>:

<link rel="stylesheet" href="/app.css">          <!-- 40 KB -->
<link rel="stylesheet" href="/admin-tools.css">  <!-- 180 KB, only used by staff -->
<script src="/payment-sdk.js"></script>         <!-- 250 KB, needed only when the guest clicks Pay -->
<script src="/analytics.js"></script>           <!-- 90 KB -->

The card images below also have no width or height. Explain what delays the first paint and propose fixes, ordered by impact.

Step 4 · Decide

1,000 pins on the hotel map

Decision Stretch

Maya wants a map next to the search results showing every hotel in the city as a pin: up to 1,000 pins in Paris. Pins move together when the guest drags or zooms the map.

Two options: - A: each pin is a DOM element (<div class="pin">) with its price label, positioned with CSS. - B: draw all pins on one <canvas>, and handle clicks by computing which pin is under the finger.

Write a short decision in this form: "We chose X over Y because…, accepting the risk of…".

15

Explain it yourself

Explain how a browser gets from an HTML file to pixels, and why animating transform is smoother than animating left.

0 words