What you'll learn
- Quick answer
- Why Long Pages Are Slow Even When They're 'Just HTML'
- content-visibility: auto, Explained
- Avoiding Layout Shift With contain-intrinsic-size
- A Real Benchmark: 232ms to 30ms
- content-visibility: hidden vs display: none vs visibility: hidden
- Browser Support in 2026
- Where to Actually Use This
- FAQ
Quick Answer
content-visibility: autotells the browser to skip layout and paint work for an element until it's actually near the viewport — the same idea as JavaScript list virtualisation, but as a single CSS property with no library. Paired withcontain-intrinsic-sizeto reserve placeholder space, it's one of the cheapest rendering-performance wins available for long pages, and every major browser has supported it since 2024.
Why Long Pages Are Slow Even When They're 'Just HTML'
Downloading HTML is only the first step. Once it arrives, the browser still has to build a render tree, compute layout (the geometry — position and size of every box) and paint every node, including everything far below the fold that nobody has scrolled to yet. A long article, a huge table, or a comment thread with thousands of nodes pays that layout-and-paint cost upfront, in full, for content the user may never even reach.
The traditional fix is JavaScript-level virtualisation — libraries like react-window that remove off-screen rows from the DOM entirely and re-insert them as the user scrolls. It works, and it works well, but it's real implementation complexity: a library dependency, careful handling of scroll position and item height, and code that has to be maintained alongside everything else. content-visibility gets a meaningful slice of the same benefit as a single CSS property, with no JavaScript and no library.
It's worth being specific about which part of "slow" this actually targets, because page-load performance has several genuinely separate costs that are easy to lump together. Downloading HTML is a network cost. Parsing it into a DOM tree is a CPU cost, but a comparatively cheap one. Computing layout — figuring out the exact position and size of every box, which can cascade: changing one element's height can shift everything below it — and then painting pixels for all of it is usually the most expensive of the three for a large page, and it's also the one most people never think to measure separately, because it doesn't show up as a distinct line item in a basic Lighthouse score the way "network" or "bundle size" do. content-visibility targets exactly this layout-and-paint cost, specifically, and does nothing for the other two.
content-visibility: auto, Explained
The core mechanism, confirmed against current browser documentation: content-visibility: auto makes the browser skip an element's layout, paint and style work while it's off-screen, treating it roughly as if it had zero height for layout purposes — without actually removing it, and without taking it out of the accessibility tree.
.section {
content-visibility: auto;
}
Once the element gets close enough to the viewport to matter, the browser renders it normally — the exact threshold is an implementation detail left to the engine, not something you configure directly. Everything else about the element — its place in the DOM, its accessibility tree entry, its reachability via scripting — stays completely normal. The only thing skipped is the visual rendering work: layout, paint and compositing, not the element's existence.
Under the hood, applying content-visibility: auto to an element also applies a set of CSS containments to it automatically — you don't add these yourself, they come bundled with the property. While the element is off-screen, it gains layout, style, and paint containment, plus size containment specifically, which is what lets the browser safely treat its height as a fixed, cheap value rather than recomputing it. This is the same underlying mechanism the separate contain property exposes on its own (for cases where you want containment without the skip-when-off-screen behaviour); content-visibility: auto is really "apply strong containment, and additionally skip rendering work entirely while off-screen," rather than two unrelated features bundled by coincidence.
Avoiding Layout Shift With contain-intrinsic-size
There's an immediate problem if you stop there: if an unrendered section collapses to roughly zero height, the page's scrollbar and scroll position jump the instant the browser actually measures the real content — a layout shift, which is exactly the kind of thing Core Web Vitals penalises. The fix is reserving placeholder space with contain-intrinsic-size:
.section {
content-visibility: auto;
contain-intrinsic-size: auto 500px;
}
The 500px is a placeholder height used before the element has ever been rendered. The auto keyword in front of it changes what happens after that first render: once the browser has actually measured the element's real rendered size at least once, it remembers that size and uses it as the placeholder going forward, instead of falling back to your hand-written guess every time. In effect, your fixed number is only a cold-start estimate — the browser self-corrects to the real size after the first real render.
Getting the initial guess reasonably close matters more than it might seem. Too small, and the page still jumps noticeably when the real content renders and turns out to be taller than your placeholder — you've reduced the shift, not eliminated it. Too large, and you've reserved more empty scroll space than the content actually needs, which can make a page feel like it has odd gaps before anything has scrolled into view and been measured for real. For a reasonably uniform list — comment threads, article sections of similar length — measuring one or two typical real examples by hand and using that as your starting number is usually close enough; for wildly variable content (some sections are a paragraph, others have embedded images and tables), it's worth accepting that the first render of each unique section will always carry some shift risk, since no single placeholder number can represent genuinely different content well.
A Real Benchmark: 232ms to 30ms
web.dev's own published demo of this property is worth citing directly, since it's a concrete, documented number rather than a vague claim: applying content-visibility: auto to chunked content sections took initial rendering time from 232ms down to 30ms in their test case — roughly a 7x improvement.
The mechanism behind that number is straightforward once you see it: skipping layout and paint for every off-screen chunk means the browser's initial rendering pass only does real geometry-and-paint work for the slice of the page that's actually visible, instead of the entire multi-thousand-node document. The win scales with how much of your page is below the fold relative to what's immediately visible — a short page won't see anything close to 7x, but a genuinely long one, with most of its content off-screen on load, is exactly the shape of page this property was built for.
It's worth being honest about what this number does and doesn't generalise to. A 7x improvement on one specific demo page with a particular amount of chunked content below the fold isn't a universal multiplier you should expect on every page you apply the property to — your own result depends entirely on how much of your actual page is off-screen on load relative to what's visible, and how expensive that off-screen content's layout and paint work would have been in the first place (a page of plain paragraphs has less layout work per node than one full of flex/grid containers and images). Treat "232ms to 30ms" as a real, verifiable existence proof that the mechanism works as described, not as a number you can predict your own page will hit without measuring it directly with the same before/after DevTools Performance recording technique used to produce it.
content-visibility: hidden vs display: none vs visibility: hidden
These three are easy to blur together, but they behave differently enough to matter for a real toggle-heavy UI like an accordion or tab panel:
| Property | Takes up layout space? | Rendering cost while hidden | Cost to show again |
|---|---|---|---|
display: none | No | None — fully removed from the render tree | Full re-render from scratch, every time |
visibility: hidden | Yes | Still does layout, just skips painting | Cheap — layout was already done |
content-visibility: hidden | No | Skips layout and paint, like display: none | Cheaper than display: none — rendering state is cached, not discarded |
The practical takeaway: content-visibility: hidden is a genuinely different tool from display: none, not just a longer way to write it. For a panel you toggle open and closed repeatedly — an accordion section, a tab you switch away from and back to — it skips the same rendering cost as display: none while hidden, but caches enough of the previous render that un-hiding it is cheaper than un-hiding a display: none element, which has to redo its layout and paint completely from zero every single time.
This makes content-visibility: hidden specifically worth reaching for in exactly one recurring situation: UI you expect to be shown and hidden repeatedly within the same page session, where the cost of re-rendering on every toggle is the thing you're trying to avoid. For something hidden once and essentially never shown again on that page view — a mobile menu that stays closed unless explicitly opened, content behind a one-time disclosure — the caching benefit has nothing to pay off, and display: none remains the simpler, more conventional choice. Reach for the content-visibility variant when the toggle itself is the thing happening often, not as a blanket replacement for every hide/show pattern in your CSS.
Browser Support in 2026
Chrome and Edge have supported content-visibility since Chrome 85 in 2020. Firefox shipped it in version 125 (2024), after sitting behind a flag in versions 109 through 124. Safari added support in Safari 18 (2024). As of 2026, every major engine supports it directly — there's no need for a JavaScript polyfill or a complex fallback strategy for most real audiences.
It's still worth wrapping usage in a feature query if you want to be thorough about an unusually old slice of traffic, since the degradation story is clean either way:
@supports (content-visibility: auto) {
.section { content-visibility: auto; contain-intrinsic-size: auto 500px; }
}
A browser that doesn't support the property just renders everything normally — slower on a very long page, but not visibly broken. There's no case where an unsupported browser shows missing or malformed content because of this property.
The gap between Chrome's 2020 ship date and Safari and Firefox both landing it in 2024 is worth noting for anyone who evaluated this property a few years ago and dismissed it as "Chrome-only" at the time — that was an accurate assessment in 2021 or 2022, but it's genuinely out of date now. If your own notes, internal docs, or a past performance audit flagged content-visibility as not viable due to limited support, it's worth revisiting that conclusion specifically, since the support picture has fully changed since then rather than just incrementally improved.
Where to Actually Use This
The strongest fit: long-form article or documentation pages with many sections, large HTML tables, comment threads and chat histories, and server-rendered infinite-scroll feeds that aren't already behind a JavaScript virtualisation library. Anywhere you have a genuinely large DOM where most of it sits below the fold on initial load is a candidate.
Two things worth being precise about. First, don't apply it to content in the very first viewport — there's no rendering work to skip for something the user sees immediately, and it adds no benefit there. Second, it doesn't quietly make content unreachable: browser find-in-page (Ctrl+F / Cmd+F) and keyboard focus navigation both force a skipped section to render on the way to it, and screen readers traverse the DOM rather than the render tree, so accessibility and findability are both preserved even though the visual rendering work is deferred. The only thing this property skips is layout, paint and compositing — never the element's existence, its place in the accessibility tree, or its reachability.
A practical rollout approach that works well on an existing site: start with the single longest, heaviest page you have — a documentation index, a long-form guide, a product category page with a huge grid — rather than trying to apply the property site-wide in one pass. Wrap each major repeating section (an article's <section> elements, each row group in a huge table, each comment in a thread) individually rather than one giant wrapper around the whole lot, since a single content-visibility: auto container covering everything below the fold gives the browser only one boundary to skip, not many — chunking it into several smaller sections gives the browser more, smaller units it can independently decide to render or skip as the user scrolls, which is closer to how the original web.dev benchmark was actually structured.
Frequently Asked Questions
Does content-visibility: auto hide content from screen readers?
No. Screen readers traverse the DOM, not the render tree, and the element stays in the accessibility tree the whole time. The property only skips visual rendering work — layout, paint and compositing — never the element's presence or reachability.
What's the difference between content-visibility: hidden and display: none?
Both skip layout and paint while hidden, but content-visibility: hidden caches enough of the element's previous rendering state that showing it again is cheaper than display: none, which has to redo layout and paint completely from scratch every time it's shown.
Do I always need contain-intrinsic-size alongside content-visibility: auto?
In almost all real cases, yes. Without it, an unrendered section collapses toward zero height, which causes a layout shift the instant the browser actually measures and renders the real content — exactly the kind of shift Core Web Vitals penalises.
Does Ctrl+F still find text inside a content-visibility: auto section?
Yes. Browser find-in-page forces a skipped section to render on the way to showing a match, and the same happens for keyboard focus navigating into a hidden-but-rendered-on-demand section.
Is this a replacement for JavaScript list virtualisation?
For many cases, yes, and with far less implementation complexity — no library, no manual scroll-position math. For extremely large lists (tens of thousands of rows) where you also want to avoid holding that many DOM nodes in memory at all, a dedicated virtualisation library that actually removes off-screen nodes from the DOM can still go further than content-visibility, which keeps every node present just unrendered.
