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.

Offscreen subtrees skip rendering work outside the viewport A scrollable document where sections inside the viewport are rendered while sections above and below the viewport have their layout and paint skipped via content-visibility auto. viewport — rendered section — rendering skipped section — rendering skipped section — laid out + painted section — laid out + painted section — rendering skipped section — rendering skipped above viewport below viewport contain-intrinsic-size reserves height for skipped sections — no scrollbar jump

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.

Lifecycle of a content-visibility auto subtree A subtree stays inert while far from the viewport, then the engine runs style, layout and paint just in time as it approaches the viewport. One subtree, two rendering states keyed on viewport proximity Far from viewport style: skipped layout: skipped paint: skipped DOM still queryable scrolls near Near viewport style: run layout: run paint: run just-in-time contain: layout style paint changes inside never dirty the doc Implied containment makes the boundary safe to skip and cheap to resume

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.

Scrollbar stability with and without contain-intrinsic-size Without an intrinsic size skipped sections collapse and the scrollbar lurches, while a reserved placeholder size keeps scroll height stable. Without contain-intrinsic-size With contain-intrinsic-size: auto 120px rendered section skipped -> collapses to ~0 scroll thumb lurches as sections resume height rendered section skipped -> reserves 120px skipped -> reserves 120px scroll height stays stable
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.

Initial layout and paint cost before and after content-visibility A trace comparison showing the baseline paying full-document layout and paint versus a viewport-bounded cost after applying content-visibility auto. Initial render cost — 200-section document Baseline Layout — all 200 sections Paint — full doc content-visibility Layout Paint remaining sections skipped until scrolled near 0 ms wall-clock main-thread time Skipped work never enters the main-thread layout and paint budget

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
Validation decision path for a content-visibility rollout A decision tree checking whether initial layout dropped and whether resuming subtrees stay under the layout-shift budget before shipping. Record trace, scroll the page Initial Layout dropped vs baseline? CLS from resumed sections ≤ 0.1? no Tune contain-intrinsic-size estimate closer to real height yes Ship

How content-visibility: auto Skips Work

content-visibility: auto is the browser’s built-in virtualization primitive: it tells the engine that an element’s subtree can be skipped — style, layout, and paint — while it is far from the viewport, and rendered only as it approaches. The element still participates in the document’s block flow, so the page’s overall structure is intact, but the expensive per-element work inside it is deferred. On a long page composed of many independent sections — a feed, a documentation page, a list of cards — this can turn a multi-second initial layout into a fraction of it, because the engine only lays out the handful of subtrees near the viewport instead of every subtree on the page.

The mechanism is containment. content-visibility: auto implicitly applies contain: layout style paint to the element while it is skipped, which is what makes skipping safe: containment guarantees the subtree cannot affect the size or styling of anything outside it, so the engine can defer its internal work without risking an incorrect outer layout. When the element scrolls near the viewport the browser lifts the skip, runs the deferred style and layout, and paints — a small burst of work amortised across scrolling rather than paid all at once on load. The trade-off, and the reason the next section exists, is that a skipped subtree has no laid-out height, so the browser needs a hint about how tall it will be.

contain-intrinsic-size and the Scrollbar Problem

A subtree the engine has skipped has not been laid out, so its rendered height is effectively zero until it is rendered. Without a countermeasure, every skipped section below the viewport collapses to nothing, the page’s total height is wrong, and the scrollbar is meaningless — it jumps and resizes as you scroll and sections render for the first time. contain-intrinsic-size is the fix: it supplies a placeholder size the engine uses for a skipped element in place of its real laid-out size, so the page reserves approximately correct height for content it has not yet rendered. Set it to a reasonable estimate of each section’s height (contain-intrinsic-size: 0 500px reserves 500px of block size), and the scrollbar becomes stable and the page height believable.

Modern engines improve on a static estimate with auto intrinsic sizing: contain-intrinsic-size: auto 500px uses 500px until the element has been rendered once, then remembers the last rendered size and uses that as the placeholder if the element is skipped again. This means a section you have scrolled past keeps its real measured height as its placeholder, so scrolling back up does not shift. The combination — content-visibility: auto with an auto intrinsic size — is the closest the platform comes to free virtualization, and the details of tuning it are covered in contain-intrinsic-size and scroll anchoring and using content-visibility for offscreen content.

/* A feed section that is skipped while off-screen and reserves stable height. */
.feed-section {
  content-visibility: auto;
  /* 'auto' remembers the real rendered height after first render;
     the 640px is the initial estimate before that. */
  contain-intrinsic-size: auto 640px;
}

When Not to Use It

Containment-based skipping is not free of caveats, and applying it blindly can cause regressions of its own. Because a skipped subtree is not laid out, in-page search (Ctrl+F), anchor-link navigation to a :target inside it, and tab-focus order all have to force it to render before they can find or reveal the content — the browser handles this automatically, but it means a jump to a deep anchor pays the deferred layout cost at that moment. More importantly, contain: paint (which content-visibility implies) clips the element, so any design that relies on overflow escaping the box — a dropdown menu, a tooltip, a sticky child that extends beyond the section — will be clipped when the section is contained. Reserve content-visibility: auto for genuinely independent, self-contained blocks, and keep it off containers whose children need to paint outside their bounds.

The right mental test is: does this subtree’s rendering depend on, or affect, anything outside it? If a section is a self-contained unit of content — an article card, a comment, a table section — it is an ideal candidate, and the layout savings scale with how many such sections sit off-screen. If it participates in overflowing UI or its height is wildly variable, either measure carefully or leave it rendered. Used with that discipline, it is one of the few optimisations that improves both initial load (fewer subtrees to lay out) and interaction (less to re-layout on mutation), which is why it sits at the centre of this section’s approach to CSS containment.

Verifying the Win in a Trace

Applying content-visibility without measuring the result is how you end up shipping the caveats without the benefit, so the change is only complete once a trace confirms it. Record a Performance profile of the initial load before and after: the “before” trace shows a single large Layout block on the Main track sized by the whole document, and the “after” trace shows a much smaller initial Layout plus a series of small Layout bursts firing as you scroll and off-screen sections render for the first time. That shift — one big upfront layout becoming many small on-demand ones — is the signature of the optimisation working. If the initial Layout does not shrink, the sections were probably already on-screen, or the containment is being defeated by a descendant that forces layout.

The second thing to verify is scroll stability. With contain-intrinsic-size set correctly, the scrollbar should hold steady and scrolling back up should not shift content; if it jumps, the intrinsic-size estimate is far from the real height, or you are on an engine without auto intrinsic sizing and a section’s real height differs sharply from the placeholder. The Layout Instability overlay makes any resulting shift visible as a highlighted region, tying this directly back to the CLS work in the metrics section. Treat a stable scrollbar and a shrunken initial layout as the two acceptance criteria, and the feature moves from “applied” to “verified working.” One further check is worth building into a regression suite: capture the initial Layout duration as a number and assert it does not grow on future commits, because it is easy for a later change — a new descendant that forces layout, or a container that quietly loses its containment — to re-enable the full upfront layout the optimisation was meant to avoid. A skipped subtree that silently stops being skipped costs exactly as much as if the feature had never been applied, and only a measured budget catches that regression before users do. The same caution applies to contain-intrinsic-size: if a redesign changes a section’s typical height and the estimate is not updated, the scrollbar drifts again, so treat the intrinsic-size value as a number that travels with the component rather than a constant set once and forgotten.

Frequently Asked Questions

Does content-visibility auto remove offscreen content from the DOM?

No. The subtree’s DOM stays intact and fully queryable from script — content-visibility: auto only skips the rendering work (style, layout, paint) while the element is far from the viewport. This is what separates it from virtualization, which removes nodes from the tree entirely. Because the nodes remain, getBoundingClientRect() on a skipped element still forces the browser to lay it out synchronously, so avoid measuring skipped subtrees in hot paths.

Will in-page find (Ctrl+F) still match text inside a skipped subtree?

Yes. Browsers render skipped subtrees on demand for in-page search, fragment navigation, and focus, so find matches and scrolls to hidden content. This is a key advantage over display: none and over most virtualization approaches, which drop the text from the accessibility and search surfaces. Anchor links and tab focus into a skipped section trigger the same just-in-time render.

What happens if I omit contain-intrinsic-size?

Skipped subtrees collapse toward zero height, so the document’s scroll height only reflects the rendered sections. As you scroll, sections render and the page grows, making the scrollbar thumb jump and the scroll position drift. Always pair content-visibility: auto with a contain-intrinsic-size estimate; the auto keyword lets the browser replace your estimate with the real last-rendered size after first paint.

How is content-visibility different from CSS containment alone?

content-visibility: auto implies contain: layout style paint, then adds conditional skipping keyed on viewport proximity. Plain contain isolates a subtree so its changes cannot dirty the rest of the document, but the browser still styles, lays out, and paints it every frame. Containment is the isolation primitive; content-visibility uses that isolation to make offscreen work safe to skip entirely.

Can a bad contain-intrinsic-size estimate hurt Core Web Vitals?

Yes. If the reserved placeholder height differs sharply from the real rendered height, the page resizes when the section resumes, registering as Cumulative Layout Shift. Measure CLS with the Layout Instability API while scrolling and tune the estimate toward the median real section height. The auto keyword mitigates this on repeat views by remembering the true size, but the first paint still uses your static estimate.