Render Tree and Layout
Where Structure Meets Style
The DOM knows what elements exist. The CSSOM knows how they should look. Neither alone can paint a single pixel. The browser must merge them into a third structure — the render tree — then calculate the exact position and size of every visible element through layout (also called reflow).
The DOM is a building's architectural blueprint — it shows every room, closet, and hallway. The CSSOM is the interior design plan — colors, materials, furniture. The render tree is the construction plan that merges both: only rooms people will actually see (not hidden storage), with their exact dimensions and positions calculated. An element with display: none is like a room that got cut from the plans — it exists in the blueprint but will never be built.
Building the Render Tree
The render tree is constructed by walking the DOM tree and, for each node:
- Check visibility — Is this node visible? Elements with
display: noneare excluded entirely. So are<head>,<script>, and<meta>elements. - Find matching CSSOM rules — For each visible node, the browser finds all CSS rules that apply and computes the final styles.
- Create a render object — A render tree node (called a "LayoutObject" in Blink, "Frame" in WebKit) with the element's computed styles.
DOM Tree: Render Tree:
html html
├── head (excluded) ├── body
│ ├── title (excluded) │ ├── div.container
│ └── style (excluded) │ │ ├── h1 "Welcome"
└── body │ │ ├── p "Content here"
├── div.container │ │ └── img (visible)
│ ├── h1 "Welcome" │ └── footer
│ ├── p "Content here" │ └── span "Copyright"
│ ├── span.hidden │
│ │ (display: none) │ (span.hidden excluded — display: none)
│ └── img │ (script excluded — not visual)
├── script (excluded) │
└── footer │
└── span "Copyright" │
display: none vs visibility: hidden vs opacity: 0
These three properties all make elements "invisible," but they have completely different effects on the render tree and layout:
| Property | In Render Tree? | Takes Up Space? | Triggers Layout? | Clickable? |
|---|---|---|---|---|
display: none | No | No | No (removed) | No |
visibility: hidden | Yes | Yes | Yes | No |
opacity: 0 | Yes | Yes | Yes | Yes |
/* Removed from render tree entirely — no space, no layout cost */
.gone { display: none; }
/* In render tree, occupies space, but invisible and not interactive */
.invisible { visibility: hidden; }
/* In render tree, occupies space, invisible, but STILL receives clicks */
.transparent { opacity: 0; }
opacity: 0 elements are invisible but still receive pointer events. Users can accidentally click invisible buttons. If you want something visually hidden AND non-interactive, use visibility: hidden or combine opacity: 0 with pointer-events: none.
The Layout Process
Once the render tree is built, the browser must calculate the exact pixel position and size of every render tree node. This is layout (reflow).
Layout is a recursive process that starts at the root and flows down the tree. For each node, the browser must determine:
- Width: Often determined by the parent (a block element fills its parent's width by default)
- Height: Usually determined by the content (children dictate the parent's height)
- Position: Where this element sits relative to its parent, siblings, and the viewport
Layout is Top-Down for Width, Bottom-Up for Height
This is the key insight most developers miss. Width flows down the tree (parent tells child how wide it can be). Height flows up (children tell parent how tall it needs to be).
Why Layout Is Expensive
Layout is inherently global. Changing one element's size can cascade through the entire tree:
- Increase a paragraph's font size → text wraps differently → paragraph gets taller → parent grows → sibling elements shift down → their children reposition
- This layout invalidation is why changing geometric properties (width, height, top, left, margin, padding, font-size) is expensive — the browser may need to recalculate layout for large portions of the tree
// This changes font-size → triggers full layout recalculation
// because text reflow affects height, which affects parent,
// which affects siblings, which affects their children...
element.style.fontSize = '20px';
Layout boundaries and containment
Not every layout change invalidates the entire tree. The browser uses layout boundaries — points in the tree beyond which layout changes don't propagate. An element with fixed width AND height acts as a layout boundary because its children cannot change its size. CSS contain: layout explicitly creates a layout boundary, telling the browser: "layout changes inside this element will never affect anything outside." This is a powerful optimization for complex pages — more on this in the CSS Containment topic.
Production Scenario: The Expanding Text Area
A collaborative document editor had severe jank when users typed in long documents. The cause: every keystroke changed the textarea's content, which changed its height (auto-resizing), which changed the document container's height, which changed the scroll position, which triggered layout on the toolbar and sidebar.
Each keystroke triggered layout on 2,000+ render tree nodes.
The fix:
- Gave the editor container
contain: layoutto isolate its layout from the rest of the page - Used
overflow: autowith a fixed outer container height so the editor's content changes didn't affect the parent's dimensions - Debounced the scroll position sync to 1 frame via
requestAnimationFrame
Result: Layout time per keystroke dropped from 12ms to 0.8ms.
| What developers do | What they should do |
|---|---|
| Think display: none and visibility: hidden are the same display: none has zero layout cost. visibility: hidden still occupies space and participates in layout calculations. | display: none removes from render tree entirely. visibility: hidden keeps it in the tree and in layout. |
| Assume layout only affects the changed element Changing height affects parent sizing, which shifts siblings, triggering layout recalculation across the tree | Layout changes can cascade through the entire tree — parent, siblings, and their descendants |
| Use top/left/width/height for animations Geometric properties trigger layout on every frame. Transforms skip layout and paint entirely — compositor-only. | Use transform: translate() and transform: scale() for animations |
| Forget that width flows down and height flows up Understanding this flow explains why height: 100% only works when the parent has an explicit height | Width is determined by parent constraints; height is determined by content |
- 1The render tree merges DOM + CSSOM and excludes invisible elements (display: none,
<script>,<head>). - 2display: none removes from render tree. visibility: hidden keeps it in the tree. opacity: 0 keeps it interactive.
- 3Layout calculates exact pixel position and size of every render tree node.
- 4Width flows down from parent to child. Height flows up from content to parent.
- 5Changing geometric properties (width, height, margin, padding, font-size, top, left) triggers layout — potentially cascading across the entire tree.
- 6Use CSS containment to create layout boundaries that prevent layout invalidation from spreading.