The content-visibility Property: Performance Gains for Long Pages
Virtualization libraries unmount off-screen DOM nodes to save render cost, but that breaks Ctrl+F and anchor links. This CSS property defers the rendering cost while keeping both intact.
Paul Radford — Full-Stack Developer & Editor. Paul is a full-stack developer and editor of template.tips. Articles here are AI-drafted and reviewed by Paul, who knows the code well enough to catch what's wrong and cut the hype.

The content-visibility CSS property lets the browser skip rendering work (layout, paint, and in some cases style) for off-screen elements, dramatically cutting initial render time on long pages. Set it to auto on off-screen containers and the browser defers their rendering cost until they scroll into view.
What Problem Does This Actually Solve?
Before content-visibility, the main lever for deferring off-screen work was virtualization: libraries like react-window or vue-virtual-scroller that unmount DOM nodes outside the viewport and re-mount them on scroll. That works, but it requires JavaScript, careful height estimation, and it breaks native browser features like Ctrl+F search, anchor links, and accessibility tree traversal for hidden content.
content-visibility: auto gives you a chunk of that performance win with a single CSS declaration and no JavaScript. The browser still knows the element exists, still includes it in the accessibility tree (when not skipped), and still lets users search the page for text inside it. It just skips the expensive parts of rendering, layout calculation, paint, and hit testing, until the element is near the viewport.
The practical effect: a 10,000-row table or a long article with 200 embedded components can render its initial paint in a fraction of the time, because the browser only pays full rendering cost for what's actually visible plus a margin.
Basic Usage
1/* Apply to off-screen sections you want the browser to skip rendering for */2.long-list-item {3 content-visibility: auto;45 /* contain-intrinsic-size prevents layout jank by giving the browser6 a placeholder size before it has rendered the real content */7 contain-intrinsic-size: 0 500px;8}The contain-intrinsic-size property is not optional in practice. Without it, unrendered elements collapse to zero height, which means your scrollbar length jumps around as elements enter and exit rendering, and scroll position calculations become unreliable. Give it a reasonable estimate of the element's rendered height. If sizes vary wildly, contain-intrinsic-size: auto 500px (supported in newer Chromium versions) lets the browser remember the last rendered size and use that as the estimate going forward, which is more accurate for lists with irregular row heights.
How Does This Differ from display: none?
This is the question most developers ask first, and the distinction matters for correctness, not just performance.
display: none removes an element from the render tree entirely. Its content is invisible to Ctrl+F, invisible to screen readers, and not indexed by the browser's internal find-in-page logic. content-visibility: auto keeps the element in the DOM and in the accessibility tree structurally, but skips the rendering subtree until it's needed. When the user does a page search, the browser temporarily renders matching sections to check for text matches, which is exactly the behavior you want for long documentation pages or archives.
1<!-- Each section skips rendering when off-screen, but remains2 searchable via Ctrl+F and reachable via anchor links -->3<section id="chapter-12" style="content-visibility: auto; contain-intrinsic-size: 0 1200px;">4 <h2>Chapter 12: Error Handling</h2>5 <p>...</p>6</section>If you use display: none for tab panels or accordions and expect users to search across them, you already have that gap today. Switching to content-visibility: hidden (not auto) preserves the render-skipping behavior of display: none while keeping the element's rendering state cached, so toggling it visible again is cheaper than an initial render. That's the actual use case for hidden: cheap re-show, not initial-load performance.
Measuring the Actual Gain
Don't take the performance claim on faith. Chrome's DevTools Performance panel shows the difference directly: record a trace loading a long page with and without content-visibility: auto, and compare the "Rendering" and "Painting" time in the summary. For a page with a few thousand off-screen nodes, it's common to see rendering time drop by 50 percent or more on initial load, because the browser is only doing layout and paint for the visible viewport plus a small buffer.
You can also check this with the Layout Instability API in the console, or simpler, just watch the Core Web Vitals overlay (Largest Contentful Paint in particular tends to improve, since LCP often depends on how fast the browser can get through layout of preceding content).
1// Quick sanity check: log LCP timing before and after adding content-visibility2new PerformanceObserver((entryList) => {3 const entries = entryList.getEntries();4 const lastEntry = entries[entries.length - 1];5 console.log('LCP:', lastEntry.startTime, lastEntry.element);6}).observe({ type: 'largest-contentful-paint', buffered: true });Run this in an incognito window with cache disabled, and test on a throttled connection (DevTools Network throttling, "Fast 3G" or similar) because the gains are much more visible when the browser is CPU or bandwidth constrained rather than running on a fast desktop with nothing else competing for the main thread.
When Should You Not Use It?
content-visibility: auto is not a universal fix, and there are real cases where it causes more problems than it solves.
If your layout depends on precise measurement of off-screen elements, for example a ResizeObserver callback that reads the height of a sibling to synchronize two columns, skipped rendering means that height comes back wrong or stale. Anything relying on getBoundingClientRect() for off-screen content will get inconsistent results depending on whether the browser has rendered that subtree recently.
CSS containment also changes how certain layout modes behave. Elements with content-visibility: auto get an implicit contain: layout style paint, which means things like position: sticky inside them can behave unexpectedly, and CSS counters or ::before/::after content that depends on sibling state may not update the way you expect until the element is rendered.
Animations and transitions on properties inside a skipped subtree simply don't run while skipped. If you have an off-screen element with an infinite CSS animation that you expect to be "already in progress" by the time it scrolls into view, it won't be. It starts from scratch when rendering resumes.
Small pages don't benefit meaningfully. If your page has 50 DOM nodes total, the rendering cost you're skipping is already trivial, and adding content-visibility just adds complexity and the risk of layout shift from bad intrinsic size estimates for no measurable win.
Browser Support and a Real Gotcha
As of early 2026, content-visibility is supported in Chrome and Edge since version 85 (released August 2020) and in Chromium-based browsers generally. Firefox shipped support in Firefox 125 (April 2024). Safari added support in Safari 18, which shipped with iOS 18 and macOS Sequoia in September 2024. Check caniuse.com/mdn-css_properties_content-visibility for exact current numbers because point releases sometimes fix edge-case bugs in how the property interacts with containment and intersection detection.
The gotcha worth knowing: because content-visibility: auto uses relevance to the viewport (effectively similar to IntersectionObserver semantics) to decide when to render, elements inside a container with overflow: hidden or a fixed height that clips them can behave differently than elements in normal document flow. If you're applying this inside a custom scroll container rather than the document's default scroller, test carefully. I've seen cases where nested scroll containers cause the browser to either never mark content as "relevant" (so it never renders) or to render everything immediately (defeating the purpose), depending on how the containment boundaries are set up.
Rolling It Out Without Breaking Things
The safest rollout path is incremental and specific to elements you can bound with a reasonable intrinsic size estimate: long comment threads, chat message histories, archived blog post lists, or repeated card grids. Start by applying it only to elements below the fold on your longest pages, measure LCP and Total Blocking Time before and after with web.dev's guide on optimizing content-visibility, and check that Ctrl+F search still finds text in the deferred sections in your actual target browsers, not just Chrome.
If you rely on any JavaScript that measures off-screen elements on page load (carousel libraries, sticky sidebar calculations, virtualized-list libraries that also read DOM sizes), audit those call sites first. The MDN reference on content-visibility documents the interaction with the contentvisibilityautostatechange event, which fires when an element starts or stops skipping its rendering, useful if you need to trigger a re-measurement exactly when content becomes relevant rather than guessing with a scroll listener.
Treat this as a targeted tool for specific long-page bottlenecks, not a blanket "add to body *" performance hack. Applied where the DOM is genuinely large and the intrinsic sizes are predictable, it's one of the few CSS-only wins that meaningfully moves Core Web Vitals numbers without touching your JavaScript bundle at all.
Related Articles

Optimizing LCP with the HTML fetchpriority attribute
Not every image on a page deserves the same loading priority from the browser's default heuristics. Here's when to mark the hero image high, and when deprioritizing the rest helps LCP more.

The CSS contain Property: Isolating Layout for Performance Wins
On a page with thousands of DOM nodes, a single style change can ripple outward and cost real, measurable render time. Containment scopes that recalculation to one isolated box instead.

How to use the advanced CSS attr() function for types and units
attr(data-color color, #333) feeding straight into background-color sounds like it already ships, but as of early 2026 it's spec-only. Here's what attr() actually does today, and what's coming.