JS Sketchbooksee JavaScript think ✏️
← back to phase 7

7.8 — How the browser renders

You've mutated the tree (7.3) and heard the word "repaint" more than once already. Today, gently, the whole pipeline behind it — and the one gap in it that causes more flaky tests than almost anything else: existing in the DOM is not the same as being visible on screen.

Four stages, in order, every single page load: Parse → Render Tree → Layout → Paint. Learn the shape once, and "why is my test clicking something that isn't there yet" stops being a mystery.

watch it happen
<!-- the HTML the server sent -->
<body>
  <h1>My List</h1>
  <p class="note">loading…</p>
</body>

<!-- the CSS the server sent -->
<style>
  .note { display: none; }
</style>

// in JS: a mutation, later
document.querySelector("h1").textContent = "Shopping";

Parsing produces TWO trees at once: the DOM (from the HTML text, 7.1 — you already own this) and a new one, the CSSOM (from the CSS text) — a tree of style rules, same shape idea, different source.

the render pipelineParseRender TreeLayoutPaintDOM tree — from HTML (7.1)CSSOM — from CSS (new!)parse produces TWO trees in parallel: the DOM you already know, and a new one —the CSSOM
under the hood

The deeper story, with the real names for things — this part is what turns “I saw it” into “I can explain it.”

"Gently" because the real pipeline has more nuance the job rarely needs day to day: layout can happen more than once per frame if JS reads a geometry property (like offsetHeight) right after writing one (forcing a "layout thrash").

And modern engines composite separate layers so some animations (transform, opacity) skip layout and paint almost entirely — which is exactly why those two properties are the fast ones to animate.

Precision on the CSSOM: it isn't optional or skippable — a page with zero CSS still builds one (with browser-default styles), because the render tree always needs computed styles paired with content, never content alone.

Job note: this whole lesson is the "why" behind auto-waiting, the single feature that makes Playwright tests dramatically less flaky than hand-rolled ones. Phase 11 teaches the API; today you own the reason it has to exist at all.

your turn

⌨️ model the render-tree exclusion

The sandbox has no real rendering — so model the ONE rule that matters most: which nodes make it into the render tree, and which don’t.

requirements:

  • An array nodes of objects: { tag: "h1", display: "block" }, { tag: "p", display: "none" }, { tag: "ul", display: "block" }.
  • Write renderTree(nodes): return only the nodes whose display is NOT "none" (4.9’s filter).
  • Print how many nodes are in the full nodes array, then how many survive into renderTree(nodes).

when you press RUN, the console must show exactly:

3
2

✏️ Quick check 1

An element has display: none. Is it part of the render tree? Type yes or no.

✏️ Quick check 2

Put these four stages in order (comma-separated): Paint, Parse, Layout, Render Tree

✏️ Quick check 3

Can an element exist in the DOM (queryable with querySelector) before it has been painted on screen? Type yes or no.

teach it back

🗣️ Now teach it back

Explain to a friend the four stages of the render pipeline, in order — and why "it exists in the DOM" is not the same claim as "it is visible on screen," with an example of a real testing consequence.

Write it as if your friend is sitting next to you. Saved to your journal — future-you will use these notes to teach others.

a few sentences, minimum — you’ve got this
to remember
Parse (DOM + CSSOM) → Render Tree (visible nodes only — display:none is fully excluded) → Layout/reflow (geometry) → Paint (pixels). Mutations partially re-run this.
Existing in the DOM ≠ visible on screen. That gap — "not visible yet" — is real and common, not an error.
display:none removes an element from the render tree (0 space); visibility:hidden keeps its space, just unpainted. This exact gap is why Playwright’s auto-waiting checks attached/visible/stable/enabled, not just DOM presence.