Saving rendering work without losing content
Long product listings, article feeds and documentation pages force the browser to calculate layout and style for thousands of elements, even though only a fraction of them is ever visible. content-visibility skips this work for offscreen regions on purpose, turning a sluggish long page into one that feels as fast as a short one.
Table of Contents
- 1. Why long pages slow rendering down
- 2. The values of content-visibility in detail
- 3. contain-intrinsic-size: preventing layout jumps
- 4. Practical example: a feed with hundreds of cards
- 5. content-visibility vs. display: none vs. visibility: hidden
- 6. Scroll position, find in page and keyboard navigation
- 7. Measuring with the DevTools rendering panel and performance trace
- 8. Combining with containment and lazy loading
- 9. content-visibility in direct comparison
- 10. Summary
- 11. FAQ
1. Why long pages slow rendering down
A browser calculates style, layout and, in many cases, paint for every element in the document, regardless of whether that element is currently within the visible viewport. On a page with fifty elements that overhead is invisible. On a product listing with two thousand cards, a comment section with hundreds of entries, or a long technical documentation page, that work adds up to noticeable delays during initial render and on every reflow. This is exactly where content-visibility comes in: the property allows the browser to skip rendering work for content outside the viewport entirely, until it is actually needed.
The key difference from classic lazy loading is that content-visibility does not delay data loading, it relieves the rendering pipeline itself. The DOM node still exists, its content is present in the accessibility tree, but layout and paint are only computed once the region enters the viewport. For pages with hundreds or thousands of elements, this is often the single most effective lever for improving time to interactive and scroll performance without changing the structure of the page at all.
2. The values of content-visibility in detail
The content-visibility property has three relevant values: visible, hidden and auto. visible is the default and changes nothing, the element renders normally. hidden removes the content from rendering entirely, similar to display: none, but unlike that keeps the element's internal state intact, such as scroll positions in nested containers or form input values. The value that matters most for performance optimization is auto: the browser decides on its own whether an element is rendered, based on whether it is inside or near the visible viewport.
With content-visibility: auto the browser skips layout, paint and in many cases style recalculation for elements outside the viewport, while keeping an approximate size for the element so the scrollbar height and the position of other elements stay stable. As soon as the element scrolls near the viewport, the browser renders it normally, usually before it becomes visible, so no popping-in effect occurs. This combination of skipped work and automatic reactivation makes content-visibility: auto the central property for long lists, feeds and documentation pages.
/* Basic content-visibility setup for a long feed of cards */
.feed-item {
content-visibility: auto;
/* Reserve space so scrollbar height stays stable
before the browser has ever rendered this item */
contain-intrinsic-size: auto 320px;
}
/* content-visibility: hidden keeps internal state,
unlike display: none which resets it completely */
.tab-panel[data-active="false"] {
content-visibility: hidden;
}
.tab-panel[data-active="true"] {
content-visibility: visible;
}
3. contain-intrinsic-size: preventing layout jumps
Without a size hint, an element with content-visibility: auto collapses to zero height while unrendered, because the browser performs no layout calculation and therefore has no idea of its natural size. The result would be a jumping scrollbar and an unstable layout as elements render in and out. The contain-intrinsic-size property solves this by defining a placeholder size the browser uses as long as the actual content is not rendered.
The value contain-intrinsic-size: auto 320px combines two behaviors: the auto keyword tells the browser to remember the last actually rendered size and reuse that size instead of the fixed value once the element has been hidden again, the fixed pixel value only serves as an initial fallback before the first render. For cards with widely varying heights, such as comments with variable text length, this combination is far more robust than a single fixed value, because it adapts to each element's real height after the first pass and keeps layout jumps to a minimum.
4. Practical example: a feed with hundreds of cards
In a typical use case, a news feed or a product overview with several hundred cards, content-visibility: auto is applied directly to the individual card containers, not to the whole list. Each card additionally gets an estimated height through contain-intrinsic-size, ideally derived from the average of already rendered cards of the same type. For a feed with mixed card types, such as text posts and image posts, a different height estimate per type is recommended so the initial layout shift on first render stays as small as possible.
A real example: a documentation page with sixty sections, each with several paragraphs and sometimes code samples, benefits enormously from content-visibility: auto at the section level. Before the optimization, the initial rendering of all sixty sections takes noticeably long, especially on weaker mobile devices. After the optimization, the browser only renders the initially visible sections, the rest is computed as the user scrolls down. The measured improvement in rendering time for such pages is often in the range of fifty to ninety percent, depending on the total number and complexity of the sections.
/* Documentation page: apply content-visibility per section,
not to the wrapping container */
.doc-section {
content-visibility: auto;
contain-intrinsic-size: auto 600px;
}
/* Mixed feed: different estimated heights per card type
reduces initial layout shift when the card first renders */
.card--text {
content-visibility: auto;
contain-intrinsic-size: auto 180px;
}
.card--image {
content-visibility: auto;
contain-intrinsic-size: auto 420px;
}
5. content-visibility vs. display: none vs. visibility: hidden
All three properties sound similar but differ fundamentally in effect. display: none removes an element from layout and the accessibility tree entirely, any internal state is lost, a video pauses, a form field is reset when shown again unless it was saved externally. visibility: hidden keeps the space in the layout but makes the element invisible and non interactive, the rendering cost stays fully intact, so no work is actually saved.
content-visibility: hidden is a middle ground: the content is not rendered and therefore consumes no layout or paint resources, but the internal state is preserved because the DOM node is not removed. For tab panels, accordions and modals where repeated showing and hiding without losing state matters, content-visibility: hidden is often the better choice over display: none, especially when the hidden panels themselves contain complex content with their own state.
6. Scroll position, find in page and keyboard navigation
A frequently overlooked aspect of content-visibility: auto concerns the browser's built in Ctrl+F search. Since unrendered content is invisible to the browser, a naive search would miss matches inside hidden regions. Modern browsers solve this through the beforematch event: as soon as the native search finds a match inside a region with content-visibility: auto, this event fires and the region is automatically rendered and scrolled into view, before the user even notices anything.
The same logic applies to keyboard navigation and anchor links: if a user jumps directly to #section-42 inside a region with content-visibility: auto, the browser automatically activates rendering for that region so the anchor target is actually reachable. This behavior is part of the specification and works without any extra JavaScript, which makes content-visibility considerably more robust than handrolled lazy rendering solutions that often do not cover these edge cases.
7. Measuring with the DevTools rendering panel and performance trace
To prove the effect of content-visibility, a subjective impression is not enough. In the Chrome DevTools performance panel, a trace recorded before the optimization typically shows long, continuous bars for Recalculate Style and Layout right after the page loads. After adding content-visibility: auto to the relevant containers, these bars shrink noticeably, because only the visible elements are computed. Additionally, the rendering tab option "Layout Shift Regions" can be used to check whether contain-intrinsic-size is calibrated correctly, visible jumps indicate an inaccurate height estimate.
For automated measurements, the browser's performance API works well, in particular PerformanceObserver with the long-animation-frame entry type for detecting long rendering blocks, along with Lighthouse for aggregated metrics like Largest Contentful Paint and Total Blocking Time. On pages using content-visibility, pay particular attention to First Contentful Paint, which usually arrives noticeably earlier thanks to the reduced initial rendering, while Largest Contentful Paint stays stable as long as the visible hero element itself is not affected by the optimization.
// Measure rendering cost reduction from content-visibility
// using the Long Animation Frames API
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`Long frame: ${entry.duration.toFixed(1)}ms`, entry);
}
});
observer.observe({ type: "long-animation-frame", buffered: true });
// Detect layout shifts caused by inaccurate contain-intrinsic-size
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
console.log(`Layout shift value: ${entry.value}`);
}
}
}).observe({ type: "layout-shift", buffered: true });
8. Combining with containment and lazy loading
content-visibility: auto automatically implies contain: layout style paint for elements whose content is currently unrendered. This means the effects described in this article overlap closely with classic CSS containment, but content-visibility goes a step further, because in addition to isolation it also allows skipping the rendering work entirely, not just containing it. For components that must always stay visible but need their rendering cost isolated from their surroundings, plain contain without content-visibility remains the right choice.
For images, content-visibility combines well with native loading="lazy": loading="lazy" delays the network loading of the image file, content-visibility: auto additionally delays the rendering work of the surrounding container. Both mechanisms complement each other because they act on different stages of the pipeline, network versus rendering, and together achieve the strongest effect for image heavy, long pages.
9. content-visibility in direct comparison
The following table contrasts the various approaches for hiding or deferring rendering work, to make the decision easier for a concrete project.
| Approach | Rendering saved | State preserved | Recommendation |
|---|---|---|---|
| display: none | Yes | No | Only for actual removal from the document |
| visibility: hidden | No | Yes | Only for pure hiding without a performance goal |
| content-visibility: hidden | Yes | Yes | Tabs, accordions, modals with state |
| content-visibility: auto | Yes, automatically | Yes | Long lists, feeds, documentation pages |
| Manual virtual scrolling | Yes, including DOM removal | Partial | Very large data sets, high implementation cost |
The most important difference in the table is between content-visibility: auto and manual virtual scrolling: virtual scrolling removes elements from the DOM completely, saving additional memory, but requires significantly more JavaScript logic and breaks standard browser features like find in page and fragment navigation unless implemented very carefully. content-visibility delivers most of the performance gain at a fraction of the complexity, because it is a native browser feature and needs no custom scroll simulation.
Mironsoft
CSS performance, rendering optimization and modern web frontends
Long pages that still scroll smoothly?
We analyze your long lists, feeds and documentation pages, identify rendering bottlenecks and implement content-visibility with correctly calibrated contain-intrinsic-size for measurably faster rendering.
Rendering audit
Performance trace analysis and identification of the most expensive layout regions
Implementation
Integrating content-visibility and contain-intrinsic-size cleanly into existing components
Measurement
Before and after comparisons with Lighthouse and Core Web Vitals
10. Summary
content-visibility is one of the most effective CSS properties for long pages, because it skips rendering work for invisible regions entirely instead of merely delaying it. The value auto activates this behavior automatically and reactivates rendering as soon as a region comes near the viewport. contain-intrinsic-size is not an optional extra here, it is necessary to avoid layout jumps, because otherwise the browser has no size information for unrendered elements.
Compared to display: none and visibility: hidden, content-visibility: hidden offers the best combination of state preservation and rendering savings for tabs and accordions. For long lists and feeds, content-visibility: auto is the right choice, with far less implementation effort than manual virtual scrolling, at a similar performance gain for most use cases. To prove the effect, measure with the Chrome DevTools performance panel and the layout shift API before and after the change.
content-visibility for long pages: the essentials at a glance
Core principle
content-visibility: auto skips layout and paint for regions outside the viewport, reactivates automatically as they approach.
Mandatory companion
contain-intrinsic-size: auto Xpx prevents layout jumps and stabilizes the scrollbar height.
State preservation
content-visibility: hidden keeps internal state, unlike display: none.
Browser features
Find in page, fragment anchors and keyboard navigation work automatically via the beforematch event.