Content-Visibility and Rendering Subtrees
The content-visibility CSS property lets the browser skip rendering work β style, layout, and paint β for subtrees that are currently offscreen, then resume it lazily as they scroll into view. Paired with contain-intrinsic-size, which reserves placeholder dimensions so skipped subtrees still contribute to scroll height, it can slash the rendering cost of long documents without virtualization. This is part of Layout and Paint Optimization, and it extends the boundaries established by CSS Containment Strategies.
The mechanism matters most for pages with many heavy, independent sections β feeds, documentation, dashboards β where the browser otherwise pays full layout and paint for content the user has not scrolled to.
This area covers applying content-visibility:auto to real layouts, the gotchas around in-page search and focus, and sizing placeholders so skipped content does not cause layout shift. For hands-on application see Using content-visibility for offscreen content, and for placeholder sizing see contain-intrinsic-size and scroll anchoring.
How Render-Subtree Skipping Works
content-visibility: auto implies contain: layout style paint on the element and adds one more behavior: when the element is not near the viewport, the browser skips style, layout, and paint of its descendants entirely. The subtreeβs DOM still exists and remains accessible to script, but the rendering pipeline treats it as inert until it approaches the viewport, at which point the engine lays it out and paints it just in time.
/* Each article is an independent rendering boundary that the
browser may skip while offscreen */
.article {
content-visibility: auto; /* skip rendering when offscreen */
contain-intrinsic-size: auto 600px; /* placeholder height; remembers real size */
}
Because the implied containment establishes the element as its own layout and paint root, geometry changes inside a skipped subtree cannot dirty the rest of the document β the same isolation principle as CSS Containment Strategies, now applied conditionally based on viewport proximity.
The Sizing Problem
If a skipped subtree contributed zero height, the scrollbar would represent only the rendered sections, and scrolling toward skipped content would make the page grow and the scrollbar lurch. contain-intrinsic-size solves this by giving the box a placeholder size the browser uses for layout while the real content is skipped.
/* Before: skipped sections collapse to ~0 height β scrollbar jumps */
.row { content-visibility: auto; }
/* After: reserve an estimate so scroll height stays stable */
.row {
content-visibility: auto;
contain-intrinsic-size: auto 120px; /* `auto` remembers last-rendered height */
}
The auto keyword is the key refinement: once a subtree has been rendered once, the browser stores its real last-rendered size and uses that instead of the estimate the next time it is skipped β so the placeholder converges on the true height after first paint.
| Phase | Condition | Cost |
|---|---|---|
| Style + layout + paint | Subtree near/in viewport | Full, as normal |
| Skipped | Subtree offscreen, content-visibility:auto |
~0 (layout/paint omitted) |
| Placeholder layout | Skipped subtree with contain-intrinsic-size |
Box reserved, no descendant work |
| Resumed render | Subtree scrolls into range | One-time layout + paint |
Measuring the Win
Record a Performance trace on a long page before and after. The win shows up as a smaller initial Layout and Paint, because the browser only renders what is near the viewport. The application-level measurement workflow β including the in-page-search and focus gotchas β is detailed in Using content-visibility for offscreen content.
The flip side is layout stability. A bad contain-intrinsic-size estimate causes the page to resize as sections render, which registers as Cumulative Layout Shift. Keeping that stable is the focus of contain-intrinsic-size and scroll anchoring.
Validation
// Confirm skipped subtrees aren't producing layout shifts as they resume
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) console.log('CLS contribution', entry.value)
}
}).observe({ type: 'layout-shift', buffered: true })
| Metric | Target | How measured |
|---|---|---|
| Initial Layout duration | Drops vs. baseline | Performance panel |
| Initial Paint area | Viewport-bounded | Rendering β Paint flashing |
| CLS | β€ 0.1 | Layout Instability API |
| Time to first render of below-fold section | One-time, on scroll | Trace timeline |