Using Content-Visibility for Offscreen Content
Applying content-visibility: auto to long lists and document sections tells the browser to skip style, layout, and paint for the parts that are offscreen, cutting initial rendering cost without writing a virtualization layer. This guide covers the application, the measured drop in rendering time, and the gotchas that bite. It builds on Content-Visibility and Rendering Subtrees, part of Layout and Paint Optimization.
Minimal Application
Apply the property to each repeating, independent block and give it an intrinsic-size estimate so offscreen items still reserve scroll height.
/* Before: the browser lays out and paints all 5,000 rows on load */
.feed-item { padding: 16px; border-bottom: 1px solid #eee; }
/* After: offscreen rows skip rendering until they near the viewport */
.feed-item {
content-visibility: auto; /* skip rendering work when offscreen */
contain-intrinsic-size: auto 96px; /* reserve height; `auto` remembers real size */
padding: 16px;
border-bottom: 1px solid #eee;
}
The browser renders only the items within a viewport-proximity margin and treats the rest as reserved boxes. As the user scrolls, sections render just in time. No JavaScript, no windowing library, no key management.
The Mechanism
content-visibility: auto implies contain: layout style paint, so each item becomes its own layout and paint root. When an item is outside the rendering range, the engine skips recomputing its descendantsβ styles, skips laying them out, and skips painting them β the subtree is still in the DOM but contributes only its reserved contain-intrinsic-size box to the page. When the item enters the range, the engine runs a one-time layout and paint for it. This is the same isolation as CSS contain property performance benchmarks measure, applied conditionally on viewport proximity. The same containment primitive is examined without the viewport gate in using contain: layout to isolate reflow scope.
Measuring the Drop
Record a Performance trace on first load, before and after, under 6Γ CPU throttling.
[Main Thread] before content-visibility
ββ Layout (38.2ms) β all 5,000 rows
ββ Paint (21.4ms)
Initial render blocked: 59.6ms
[Main Thread] after content-visibility
ββ Layout (4.1ms) β ~30 visible rows
ββ Paint (2.3ms)
Initial render blocked: 6.4ms β within 16.6ms budget
The initial Layout and Paint events shrink to the cost of what is on screen. Confirm with the Rendering β Paint flashing overlay: only viewport-adjacent items repaint while you scroll.
Gotchas
In-page search (Ctrl+F)
Browsers will reveal a content-visibility: auto subtree when find-in-page matches text inside it, because the find-in-page implementation forces rendering of matched offscreen content. This works, but it means a search can trigger a burst of layout as multiple sections render at once. It is correct behavior β do not try to defeat it β just be aware the page may shift as matches resolve.
Focus and tab order
Focusable elements inside a skipped subtree remain focusable. Tabbing or programmatic focus() into a skipped section forces it to render so the focused element can be displayed. Avoid auto-focusing deep into a long offscreen list on load, which would render everything up to that point.
Anchor links and :target
Navigating to a #fragment inside a skipped subtree scrolls to it and forces that subtree to render. This works, but if you measure geometry immediately after setting location.hash, the layout may not be settled β defer the read, the same way you would for any forced synchronous layout.
Accessibility tree
Skipped subtrees still expose their content to assistive technology, so screen-reader navigation and document outline remain intact. Do not assume offscreen-skipped means hidden β it is not display:none.
Fix Pattern for Variable Heights
When item heights vary widely, a single estimate causes scroll-position drift. Set the estimate per row from a measured average, or rely on the auto keyword so the browser substitutes each rowβs real last-rendered height after first render.
.feed-item {
content-visibility: auto;
contain-intrinsic-size: auto 96px; /* `auto` overrides 96px once measured */
}
The detailed treatment of placeholder sizing and the scroll-anchoring shifts a bad estimate causes is in contain-intrinsic-size and scroll anchoring.
Verification Checklist
| Metric | Target | How measured |
|---|---|---|
| Initial Layout duration | < 4ms | Performance panel |
| Initial Paint area | Viewport-bounded | Rendering β Paint flashing |
| Items rendered on load | ~viewport count, not total | Trace node count |
| CLS from scrolling | β€ 0.1 | Layout Instability API |
Choosing Which Content to Skip
content-visibility: auto pays off in proportion to how much off-screen, independent content a page carries, so the first step is recognising good candidates. A long feed of cards, a documentation page of stacked sections, a comment thread β anything composed of self-contained blocks that stack vertically and do not need to paint outside their own bounds β is ideal, because each skipped block removes its style, layout, and paint from the initial pass. The savings scale with the number of such blocks below the fold, which on a long page can be most of the document.
The content to keep rendered is anything whose rendering escapes its box or is needed immediately: a sticky header, an element with a dropdown or tooltip that overflows, or content targeted by an in-page anchor you expect users to jump to. Because content-visibility implies contain: paint, overflowing children are clipped while the element is contained, and a jump to a deep anchor pays the deferred layout at that moment. Matching the property to genuinely independent, self-contained blocks β and pairing it with a contain-intrinsic-size estimate so the scrollbar stays honest β is what delivers the load-time win without the caveats biting.
Frequently Asked Questions
Does content-visibility: auto remove elements from the DOM?
No. The subtree stays in the DOM and remains reachable by querySelector, the accessibility tree, and in-page search. The browser only skips style, layout, and paint for it while it is offscreen, and each skipped subtree still contributes its contain-intrinsic-size box to scroll height. It is not display:none.
Why does my scrollbar jump when I use content-visibility?
The jump comes from a wrong contain-intrinsic-size estimate. When a row renders and its real height differs from the placeholder, the reserved box resizes and scroll position drifts. Use the auto keyword so the browser substitutes each rowβs last-rendered height, and see contain-intrinsic-size and scroll anchoring for the sizing detail.
Should I put content-visibility on the scroll container or the items?
Put it on each repeating item, not the container. The container itself is usually onscreen, so containing it saves nothing, while containing each independent row lets the engine skip the offscreen majority. Apply the property to the smallest independent block that repeats.
Can content-visibility replace a virtualization library?
For long lists of independent blocks, often yes β it skips offscreen rendering with no JavaScript, no windowing, and no key management. It does not recycle DOM nodes, so extremely large lists still pay DOM-size costs in memory and query time; a windowing library that keeps only a few hundred nodes mounted still wins there.
Does an anchor link into a skipped subtree still work?
Yes. Navigating to a #fragment inside a skipped subtree scrolls to it and forces that subtree to render. If you read geometry immediately after setting location.hash, defer the read one frame so layout settles, the same as you would for any forced synchronous layout.
Related Guides
- Content-Visibility and Rendering Subtrees β the parent overview of subtree rendering control and where this fits.
- contain-intrinsic-size and scroll anchoring β get the placeholder sizing right to kill scroll drift.
- CSS contain property performance benchmarks β measured cost of the containment this property implies.
- Using contain: layout to isolate reflow scope β the same isolation without the viewport gate.
- Forced synchronous layouts β why deferring geometry reads after a subtree renders matters.