Content-Visibility: Auto Makes Long Pages Faster, Until It Shifts Everything Below

content-visibility: auto lets the browser skip laying out offscreen content, making long pages load and scroll faster with two lines of CSS. The catch is that it reserves space with a guess, so get contain-intrinsic-size right or you trade slow renders for layout shift.

content-visibility: auto makes long pages faster, until it shifts everything below

content-visibility: auto makes long pages faster, until it shifts everything below

Most performance work focuses on the first paint. But a lot of slow, janky pages are not slow to start: they're slow to scroll. A dashboard with 200 cards, a docs page with 40 sections, a marketplace listing with hundreds of rows, the browser lays out and paints all of it the moment it loads, even the parts you cannot see. That wasted work shows up as janky scrolling, a big main-thread spike, and a memory bill you never asked for.

content-visibility: auto is the CSS feature that tells the browser to stop doing that. Put it on a container, and the browser skips layout and paint for whatever is offscreen. When you scroll a piece into view, it renders it on demand. The initial load gets lighter and scrolling gets smoother, and there is no JavaScript, no IntersectionObserver, and no virtualization library in the loop. It is just two properties.

What the browser actually skips

The property has three useful values. visible is the default and does nothing. hidden removes the element from layout entirely, collapsing its size to zero. auto is the one you want: it decides for each element, based on whether it sits near the viewport, whether to render it or to skip it.

Here is the subtle part. Setting auto does not just skip painting. The browser forces a set of containment rules on the element: layout, paint, style, and size. That means the element and its descendants are treated as a sealed unit. Changes inside it can not reach out and reflow the rest of the page, and the page can not reach in. That sealing is what makes the skipping safe and what makes the win real, but it also introduces the gotchas I will cover below.

As elements cross the threshold, the browser fires a contentvisibilityautostatechange event. That is genuinely useful. If a card contains a canvas chart or a looping video, you can listen for the event and pause the animation when the card scrolls offscreen, then resume it when it comes back. That gives you free CPU back with no polling and no observer code.

.card {
  content-visibility: auto;
  contain-intrinsic-size: auto 320px;
}

card.addEventListener("contentvisibilityautostatechange", (e) => {
  e.target.checkVisibility({ checkVisibilityContext: true })
    ? chart.resume()
    : chart.pause();
});

The one line that keeps the page from shifting

Here is where people get burned, and it is the reason a lot of teams try content-visibility: auto, watch their CLS score tank, and write the whole feature off as broken.

When the browser skips an element, it still has to reserve space for it, or everything below jumps up to fill the gap the moment the element renders. That reservation is contain-intrinsic-size. If you give it a fixed number that is wrong, you get layout shift. Give a 400-pixel card an intrinsic size of 320 pixels and, when it finally renders, it is 80 pixels taller than the space you left and the whole page below it shoves down. That is a CLS event, and it is on you, not on the browser.

The fix is a keyword, not a guess. contain-intrinsic-size: auto 320px tells the browser to use the element's last actually-measured size and only fall back to the number if it never rendered. So after a section has been scrolled into view once, the reserved space is exactly right and there is no shift. The fallback length only matters the very first time, before anything has rendered, and there you are guessing regardless.

Where it earns its keep

  • Long card and list views where most items sit offscreen.
  • Docs and marketing pages with many sections, where first-paint work is dominated by content nobody has read yet.
  • Dashboards and data tables with hundreds of rows, where layout cost grows linearly with row count.
  • Anything with expensive descendants: canvases, maps, iframes, heavy SVG.

The pattern is the same everywhere. Wrap the repeated or heavy block in an element you can apply auto to, add contain-intrinsic-size: auto with a reasonable fallback, and let the browser do the bookkeeping.

The traps to know before you ship it

Containment is a double-edged tool, and a few edge cases bite in production.

  • Anchor links. Clicking a jump link to a skipped section can land in the wrong place because the browser is still estimating its size, so scroll-into-view code needs to account for it.
  • Screen readers. Offscreen, skipped content is still in the accessibility tree, but some assistive tech handles the containment inconsistently, so test with a real screen reader before relying on it for content that must be announced correctly.
  • Print. Skipped content will not render in the print layout, so for print-oriented pages reset the property inside a print media query.
  • Fixed and sticky. Positioning inside a contained element is confined to it, so a sticky header you expect to span the page stops working if it nests under a contained block.
  • Do not put it on the whole body or on elements that global scripts measure. The containment can break assumptions about where things are.

Ship it progressively

The property is supported across modern browsers, but it is still the kind of thing where a graceful fallback is cheap insurance. Guard it so older engines simply render everything normally instead of doing nothing useful.

@supports (content-visibility: auto) {
  .card {
    content-visibility: auto;
    contain-intrinsic-size: auto 320px;
  }
}

That one guard means the feature is pure upside where it is supported and invisible where it is not.

Long pages have always paid a layout tax on content nobody was looking at. content-visibility: auto deletes most of that tax with two lines of CSS. The catch is that it moves the problem from a slow render to wrong space reserved, so the contain-intrinsic-size: auto line is not optional polish. It is the difference between a win and a CLS regression. Get that line right and the rest is just free.

Comments