content-visibility: auto Without the Layout Shift

content-visibility: auto tells the browser it may skip style, layout and paint for an element while that element sits offscreen. The work moves from first paint to the moment the reader scrolls near it. On a long page that is a large saving, and it costs one declaration plus a second one you have to get right.

The second one is contain-intrinsic-size. Leave it out and you have traded a rendering cost for a layout-shift bug.

The web.dev demo puts a number on the first half of that. A travel blog chunked into story sections went from 232ms of rendering time to 30ms once the property was applied to each chunk, with no change to what was downloaded.

What content-visibility: auto actually skips

auto switches on layout, style and paint containment permanently. While the element is not relevant to the user it additionally skips rendering its contents and applies size containment.

Relevant to the user has a precise definition in CSS Containment Module Level 2. Any one of four conditions is enough: the element’s overflow clip edge intersects the viewport or a user-agent defined margin around it, the element or its contents are focused, the element or its contents are selected, or the element or its contents are in the top layer. That viewport margin is deliberately left to the browser. Do not write anything that assumes a threshold.

Those three permanent containments have side effects that have nothing to do with speed. Layout and paint containment each make the element a new stacking context, a containing block for position: absolute and position: fixed descendants, and a new block formatting context. Paint containment also clips descendants at the overflow clip edge, which for a normal element is the padding box.

So a dropdown that used to escape its card will get cut off at the card’s edge. A position: sticky child can no longer stick past the container. Audit for that before you push the property through a component library, because it will not show up in a performance profile.

The good part: content inside an auto element stays in the accessibility tree, stays reachable by find-in-page, and stays focusable. The browser renders it on demand when any of those happen. That is the argument for this over an IntersectionObserver that unmounts rows, which loses all three.

Why the scrollbar jumps, and what contain-intrinsic-size does

Size containment lays the element out as though it had no children. A section that would be 800px tall reports zero height while it is being skipped. Do that to two hundred cards and the document is suddenly a fraction of its real height, so the scrollbar thumb is wrong, and every card that renders as you scroll pushes the ones below it down.

contain-intrinsic-size supplies a placeholder size for exactly this case:

.story-section {
  content-visibility: auto;
  contain-intrinsic-size: auto 600px;
}

Two things are going on in that value. The 600px is your estimate, used the first time the browser needs a size and has never rendered the element. The auto keyword says: once this element has been normally rendered, remember its real size and use that instead of the estimate whenever it goes back to being skipped. So the guess only has to survive until the reader scrolls past it once.

Chromium has implied contain-intrinsic-size: auto from content-visibility: auto since Chrome 117, released September 2023, specifically to stop elements changing size on the second pass. Write it out anyway. It costs nothing, it keeps the behaviour off a browser-version detail, and none of it rescues you on first load, when nothing has been rendered yet and there is no remembered size. The explicit length is the part doing the work there.

Get the estimate from a real measurement rather than a round number you like. If it is badly wrong the scroll position drifts as the reader moves down the page, which is the same failure the property was meant to prevent, just slower. For a grid where the track already fixes the width, constrain one axis only:

.feed-item {
  content-visibility: auto;
  contain-intrinsic-block-size: auto 320px;
}

Which elements should get it

Repeated, self-contained, below-the-fold chunks. Article sections in a long read, rows in a product grid, items in a feed, comments, the sixty accordion panels on a documentation page.

Do not put it on every element. An element that is visible from the first frame never skips anything, so all it gets from the declaration is the clipping and the stacking context. Applying it to a page header or a hero buys nothing and can break the layout you already had. Chunk the page into sections and target the sections.

Nesting is also worth avoiding. If a card already has content-visibility: auto, its children get nothing extra from having it too, and each one adds another stacking context and another clip edge.

When it makes things worse

Anything that forces the browser to measure a skipped subtree cancels the saving. getBoundingClientRect(), reading offsetHeight, scrollIntoView() on a descendant: the browser has to lay the subtree out to answer, so you pay the cost you were deferring, and often at a worse moment. Chromium prints a console warning when this happens on a skipped subtree, which is the cheapest way to find offenders in existing code.

Watch for this in analytics scripts, sticky-header measurement code, masonry layouts and anything that computes its own scroll spy.

The other common disappointment is expecting it to move Largest Contentful Paint when render delay was never the bottleneck. If your LCP element is an image that starts downloading late, skipping work on content below the fold will not help. Confirm where the time is actually going first. Our write-up on diagnosing Interaction to Next Paint covers the same discipline for the interaction side.

content-visibility: hidden is a different tool

hidden skips the contents unconditionally, regardless of where the element is. Find-in-page will not reach them, they are not focusable, and they cannot be selected. Treat that as display: none for user-agent purposes.

What you get in exchange is the reason to use it: unlike display: none, the rendering state is preserved, so showing the element again is cheap. web.dev cites Facebook measuring up to 250ms saved when returning to a cached view this way. Inactive tab panels and client-side route caches fit that description. Anything a reader might reasonably expect Ctrl+F to reach does not, so an accordion of FAQ answers is the wrong place for it.

Pausing work when an element is skipped

If a section runs an animation, a canvas or a chart, skipped rendering does not stop your JavaScript. Listen for the state change and stop it yourself:

const panel = document.querySelector('.chart-panel');

panel.addEventListener('contentvisibilityautostatechange', (event) => {
  if (event.skipped) {
    stopChartAnimation();
  } else {
    startChartAnimation();
  }
});

ContentVisibilityAutoStateChangeEvent landed in Chrome 108 (November 2022), Firefox 125 and Safari 18, matching the support for content-visibility itself in the latter two.

Can you ship it today

Per MDN, content-visibility shipped in Chrome 85 (August 2020), Firefox 125 (April 2024) and Safari 18 (September 2024), which makes it Baseline newly available as of September 2024. contain-intrinsic-size is older and wider: Chrome 83, Firefox 107 and Safari 17, Baseline since September 2023.

No polyfill is needed, because a browser that does not understand the declaration ignores it and renders the page the way it always did.

What I would do: take one long page, chunk it into sections, put content-visibility: auto with a measured contain-intrinsic-size on the sections below the fold, and profile the load in the Performance panel before and after. If rendering time was not a meaningful share of your load in the first place, stop there and go fix the thing that was; if the delay is in getting the next document requested, prefetch and prerender rules are a different lever and a cheaper one to reach for. If rendering time was the problem, roll the same pattern out to the other long templates and leave short pages alone.

Whoooop builds and tunes front ends where this kind of thing decides whether a page feels quick. If a long template on your site is slow to render and you would rather have someone measure it than guess, that is what our site speed work involves.

Need this built properly?

Whoooop Ltd has spent 15+ years building and maintaining web applications in TypeScript, React, Node.js and serverless — the same ground this post covers.

Get in touch