Your blog index renders 200 cards. Your changelog has 800 entries. Your dashboard ships a feed that takes 280ms to paint on a mid-range Android. The standard fix is react-window, vue-virtual-scroller, or chopping the page into pagination — a dependency, a refactor, or a worse UX.
There’s a one-line CSS property that tells the browser to skip layout, paint, and hit-testing for everything outside the viewport. That property is css content-visibility, and it’s been Baseline Widely Available since 2024. Most senior devs still haven’t shipped it. Here are three production patterns to copy-paste, the contain-intrinsic-size gotcha that will break your scrollbar, and how to prove the win in Chrome DevTools.
What CSS content-visibility Actually Skips
The property is content-visibility: auto. It applies layout, style, and paint containment to the element. When the element is off-screen, it also gains size containment — the browser skips layout, paint, and hit-testing for the entire subtree until the user scrolls near it. It’s a native way to skip rendering off-screen elements css, no JavaScript required.
It is not display: none. The element stays in the DOM, in the accessibility tree, and is searchable with Cmd+F. Screen readers see it. Anchor links can scroll to it. Your JavaScript can query it.
It is not lazy loading. Images inside still load on their normal schedule. What gets deferred is the browser’s rendering work — calculating where 47 nested flexbox children land, painting them, building hit-test regions. On a card with a thumbnail, headline, three buttons, and a quote block, that work adds up fast across 200 cards.
The web.dev team measured a chunked travel blog dropping from 232ms to 30ms of rendering time — a 7x improvement. That’s a best case on a contrived demo. In real production work, budget for 30-60% off your layout-and-paint phase. For anyone focused on css rendering optimization 2026, still worth the one line.
The property takes three values: visible (default), auto (the one you’ll use), and hidden (a cached-state trick for tab panels, covered in pattern 3).
That’s the mental model. Here’s the code.
Three Production Patterns You Can Ship Today
I’ve shipped this on three UI shapes where the impact was immediate and measurable. In order of value-per-effort.
Pattern 1: The Card Feed
Blog index, dashboard widget grid, comment thread, search results. Anything that’s a vertical stack of repeating sections with non-trivial DOM per item.
Apply it to the child cards, not the grid container:
.card {
content-visibility: auto;
contain-intrinsic-size: auto 420px;
}
That’s it. The browser now skips layout and paint on any card more than roughly one viewport off-screen. As the user scrolls, cards render just before they enter view.
One detail people get wrong: do not put content-visibility: auto on the grid container itself. The grid needs to compute its full layout to position the cards. Containment on the container fights that. Apply it to each child, where the heavy DOM actually lives. This is the content-visibility css performance win showing up where the DOM density is.
This composes cleanly with the rest of modern CSS. If you’re already using CSS container queries or subgrid, the containment is per-card, and your grid math is unaffected.
Pattern 2: The Long Data Table or List
500-row tables. A changelog that scrolled into the thousands. A log viewer. Anywhere you have a flat list of similar rows.
The trick is that yes, it works on <tr>:
tbody tr {
content-visibility: auto;
contain-intrinsic-size: auto 48px;
}
Tables are stricter about layout than divs, so make sure the table itself has table-layout: fixed and explicit column widths. Otherwise the browser has to render every row to figure out column sizing, and you’ve gained nothing.
Same pattern for <li> in long unordered lists, or <details> elements in a long FAQ. The rule of thumb: apply to the repeating leaf, not the container.
Ship this as-is and the scrollbar will jump. That’s a real problem, and the next section solves it.
Pattern 3: Tab Panels with content-visibility: hidden
This is the underused one. For tab interfaces and accordions, the standard pattern is display: none on inactive panels. That works, but every time the user switches tabs, the browser does a full first-render of the new panel.
Swap display: none for content-visibility: hidden:
.tab-panel[hidden] {
content-visibility: hidden;
}
The browser unrenders the panel but caches its rendered state. Switch back to a previously-viewed tab and re-show is dramatically faster — the layout and paint work was preserved. The element is still hidden from layout flow and not part of the tab order (manage aria-hidden separately).
This is the same trick a virtualizer can’t do. It also means inactive panels are not searchable with Cmd+F, which is a feature for tabs but a bug for accordion FAQs — use auto for the FAQ case, hidden for true tab UIs.
Three patterns. Copy them, ship them — and watch the scrollbar dance.
The contain-intrinsic-size Gotcha That Breaks Layouts
Drop content-visibility: auto on a card without contain-intrinsic-size and the off-screen card takes up zero pixels. Multiply by 200 cards and your scrollbar thinks the page is roughly 200 cards shorter than it actually is. As the user scrolls, cards render at their real height, the scrollbar position recalculates, and the page jumps. Your Cumulative Layout Shift score goes from 0.02 to 0.4. The contain-intrinsic-size css property exists to stop exactly this.
The naive fix is a fixed value:
.card { contain-intrinsic-size: 420px; }
Works if every card is exactly 420px. Real cards aren’t. Headlines wrap to two lines, quote blocks vary, embeds inject themselves. Under-allocate and you get the same jitter. Over-allocate and your scroll length is wrong in the other direction.
The right answer in 2026 is the auto keyword:
.card { contain-intrinsic-size: auto 420px; }
That tells the browser: use 420px as the initial placeholder, but once you’ve rendered each element once, remember its real size and use that next time. The first scroll-down may shift slightly. Every scroll after — including scroll-back — is smooth.
Picking the 420px starting value: measure the median card height in your real data, not the smallest. Under-allocating causes more scrollbar correction than over-allocating.
One more trap, and this one bites hard. Any JavaScript that reads offsetHeight, getBoundingClientRect(), or scrollHeight on a content-visibility subtree forces the browser to render it immediately — synchronously, blocking the main thread. The optimization vanishes. Common offenders: scroll-position libraries, tooltip positioning code, animation libraries that measure their targets.
Use ResizeObserver and IntersectionObserver instead. They observe without forcing layout.
Now you know the pattern works. Time to prove it.
How to Measure the Gain in DevTools
Open Chrome DevTools, Performance panel, hit record, reload the page, stop. In the timeline, look at the Rendering and Painting tracks. Before content-visibility, you’ll see a wide band across the whole subtree. After: a narrower band, scoped to elements near the viewport.
The number that matters is in the summary at the bottom — “Rendering” plus “Painting” time. On a real card feed, expect a 30-60% drop. If you’re seeing the web.dev 7x number, your before-state was extreme; nice, but don’t pitch the team on it.
Run Lighthouse before and after. The metrics to watch:
- CLS should stay flat or improve. If it went up, your
contain-intrinsic-sizeis wrong — go back to the previous section. - LCP should be neutral or slightly better. The largest contentful paint usually happens in-viewport, where containment doesn’t help.
- INP often improves on scroll-heavy pages, because hit-testing skips work on off-screen subtrees.
For real-user monitoring, the web-vitals library or PerformanceObserver will track these in production. If you’re chasing INP specifically, the INP fix guide covers what else to measure.
Quick sanity check: in DevTools, toggle content-visibility: auto off in the Styles panel and re-record. If you can’t see the difference, you don’t need this property on this page.
When NOT to Use content-visibility
A short page — content fitting in two or three viewports — doesn’t need this. The containment overhead exceeds the rendering savings. Measure first; if your layout-and-paint time is already under 30ms, look elsewhere on the web performance checklist.
Anywhere your JavaScript calls offsetHeight, getBoundingClientRect(), or scrollIntoView({ behavior: 'smooth' }) on the subtree. Forced layout reverses the optimization. Audit your scroll listeners and animation libraries before shipping this wide.
Print stylesheets need a guard:
@media screen {
.card {
content-visibility: auto;
contain-intrinsic-size: auto 420px;
}
}
Print engines handle containment inconsistently, and printing a page with hidden content is usually the wrong default anyway.
Find-in-page works — the browser will render off-screen auto elements when the user searches — but the scroll-to-result animation can show a brief reflow. Acceptable for most cases. If your page is anchor-link heavy, test the scroll-to-anchor behavior before shipping.
When you genuinely need DOM recycling — 10,000+ items, infinite scroll, virtualized rows in a spreadsheet — content-visibility alone won’t save you. The DOM is still 10,000 elements. Keep react-window for that case; that’s true css virtual content rendering territory, and it belongs to JavaScript. content-visibility is for tens to low hundreds of complex sections, not tens of thousands of simple rows.
The Bottom Line
You opened this article wondering why a 200-card feed needed a 30KB virtualization dependency. In 2026, for most long pages, it doesn’t.
The ship recommendation: add content-visibility: auto and contain-intrinsic-size: auto [median-height]px to your repeating leaf elements — cards, rows, list items, comments. Skip the grid container, skip the article wrapper. Apply where the heavy DOM lives. If you have meaningful legacy browser traffic, wrap in @supports (content-visibility: auto) — in 2026, most teams don’t.
The honest line: if you have 10,000+ DOM nodes or you need recycling, virtualization libraries still win. For the other 90% of long pages, this is one CSS rule, zero dependencies, and a measurable rendering win.
This is the rare CSS property that’s a free performance win when you use it right and an invisible footgun when you skip the contain-intrinsic-size value. Now you know how to do both. Ship css content-visibility on your next long page and measure the drop.