CSS Containment & content-visibility: Rendering Less, Less Often
This topic is part of Rendering & CSS Performance. Browsers are conservative about rendering because CSS is global: a change to one element's size can, in principle, move every element after it, change the height of the page, and alter what is painted anywhere. When something changes, the browser must work out how far the effect reaches, and on large, complex pages that analysis — and the resulting style, layout and paint work — can be expensive.
CSS containment lets authors promise that parts of the page are independent. With contain: layout, an element's internal layout cannot affect anything outside it. With contain: paint, its contents do not paint outside its box. With contain: size, its size does not depend on its children. With contain: style, counters and quotes inside do not leak out. Given these promises, the browser can limit work to the subtree that changed.
content-visibility: auto goes a step further: for elements that are offscreen, the browser can skip style, layout and paint of their contents entirely, treating them as a box of a placeholder size. On long pages — articles, documentation, feeds, comment threads, product listings — that can reduce initial rendering work dramatically. The companion property contain-intrinsic-size supplies the placeholder size, and with the auto keyword the browser remembers the last rendered size, keeping scrollbars stable.
The Metric Degradation This Topic Addresses
Rendering cost on large pages shows up in several places. On load, the first layout of a long page delays LCP render time and increases Total Blocking Time as the browser lays out content nobody can see yet. During interaction, every change that invalidates layout — expanding a comment, updating a counter, opening a menu — can trigger work across the whole document, adding to the presentation delay of INP. On pages with frequent updates (live scores, dashboards, chat), small changes in one widget may repeatedly re-layout unrelated parts of the page.
In the Performance panel, these appear as large Layout and Recalculate Style events, sometimes covering thousands of nodes for a tiny change. The "Layout" event details show how many nodes needed layout and the root of the layout; a root of #document for a small widget update is a sign that containment could help.
Prerequisites
- A Performance panel recording of page load and representative interactions, with CPU throttling.
- An understanding of the page's structure: which regions are long and offscreen, which widgets update independently.
- Visual regression testing or careful manual checks, since containment can change rendering (clipping, sizing).
1. Environment Setup: Profile Rendering Work
Record a load with 4x CPU throttling. Note total time in Layout and Recalculate Style, the number of nodes affected, and the layout roots. Then record common interactions and inspect the same events. Use the Rendering drawer's "Layout Shift Regions" and "Paint flashing" to see what repaints.
// Count elements and estimate offscreen share at load.
const all = document.querySelectorAll('body *').length;
const vh = innerHeight;
const offscreen = [...document.querySelectorAll('main > section, article > section')]
.filter((s) => s.getBoundingClientRect().top > vh * 2).length;
console.log({ all, offscreenSections: offscreen });
// trade-off: getBoundingClientRect forces layout; run this only as a diagnostic.
2. Capture a Baseline
Record the rendering time on load (style + layout + paint) and for two or three interactions, on a mid-range device profile. In the field, segment INP and LCP by template.
3. Isolate Independent Regions
Identify regions that are independent and either offscreen at load (long article sections, feed items, footers, comment lists) or updated frequently in isolation (widgets, sidebars, live counters, chat panels). These are the candidates for content-visibility: auto and contain respectively.
4. Apply Containment and Measure
Add content-visibility: auto with contain-intrinsic-size: auto <estimate> to long offscreen sections, and contain: layout paint (or contain: content, which combines layout, paint and style) to independent widgets. Re-profile and compare.
.feed-item, .article-section, .comment-thread {
content-visibility: auto;
contain-intrinsic-size: auto 600px;
}
.live-score, .chat-panel, .ad-slot {
contain: content; /* layout + paint + style */
}
/* trade-off: paint containment clips overflow (tooltips, dropdowns, shadows),
so check every contained widget for content that must escape its box. */
Deconstructing What content-visibility Skips
When an element with content-visibility: auto is far from the viewport, the browser treats it as if it had size containment and skips its contents: no style recalculation for descendants, no layout, no paint. The element itself still takes up space — the contain-intrinsic-size placeholder — so the page's total height and scroll position remain roughly correct. As the element approaches the viewport, the browser renders its contents, and with auto in the intrinsic size, remembers the actual size for next time.
The contents remain in the DOM and the accessibility tree. Find-in-page, anchor navigation and focus still work: the browser renders a skipped element when it needs to (for example, when find-in-page matches inside it). This distinguishes it from virtualization, which removes elements from the DOM entirely. The trade-off is that the DOM still holds all elements, so memory and some costs (such as DOM queries) are unchanged; only rendering is skipped.
Advanced Diagnostics and Edge Cases
Scrollbar jumps. If intrinsic size estimates differ greatly from real sizes, the scrollbar thumb jumps as sections render. Use auto with a realistic estimate; see the dedicated guide.
Sections near the fold. Content just below the viewport is rendered almost immediately as the user scrolls; applying content-visibility there adds overhead without saving much. Start a screen or two below the fold.
Sticky and fixed positioning. Containment creates a containing block for fixed-position descendants under some values, and paint containment clips; sticky headers inside contained elements may behave differently. Test carefully.
Layout-dependent JavaScript. Scripts that measure skipped elements force them to render, cancelling the benefit and possibly causing forced layout.
Browser support. content-visibility is supported in current Chromium, Firefox and Safari versions, but older versions ignore it, which is harmless.
Validation and Budgeting
Compare rendering time on load before and after; on long pages, reductions of 50–90% in initial layout time are common. Scroll through the whole page checking for visual glitches, scrollbar jumps and layout shifts (CLS should not increase). Test find-in-page, anchor links and keyboard navigation through skipped sections. Budget rendering time for key templates in lab runs, though DOM size is often the more stable budget metric.
| Check | Expected outcome |
|---|---|
| Initial layout time (long pages) | Large reduction |
| CLS while scrolling | No increase |
| Find-in-page and anchors | Work as before |
| Interaction layout roots | Contained widget, not #document |
Worked Example: A Documentation Platform
A documentation platform rendered API reference pages with up to 40,000 elements (long tables of parameters). On a mid-range phone, initial layout took 620ms and the page's INP for expanding a section was 380ms. The team applied content-visibility: auto; contain-intrinsic-size: auto 1200px to each top-level reference section, and contain: content to the collapsible blocks. Initial layout dropped to 140ms, LCP improved by 350ms, and expanding a section now laid out only that block, bringing INP to 160ms. A few anchor links landed slightly off at first because of size estimates; switching to per-section estimates from the build fixed it.
Containment for Widgets That Update Often
Pages with frequent updates — live tickers, timers, notification counts, chat — benefit from containment even when they are short. Without it, a text change in a counter may invalidate layout for its ancestors, and in some layouts, the whole page. With contain: content on the widget (and ideally fixed dimensions so contain: size can apply too), updates stay inside the widget. The Performance panel shows the layout root changing from the document to the widget, and the layout cost of each update dropping to near zero.
Interaction with Accessibility
Because skipped content stays in the DOM, screen readers can still access it, and headings in skipped sections still appear in navigation lists. However, some assistive technology interactions may cause the browser to render skipped content, temporarily cancelling the performance benefit — which is the correct trade-off. Avoid using content-visibility: hidden (which hides content like display: none but preserves rendering state) as a replacement for proper hidden semantics; it is intended for cached views such as inactive tabs.
Choosing Between Containment, content-visibility and Virtualization
These three techniques overlap, and choosing well saves effort. Use contain when content is on screen and changes often: it does not skip anything, it only limits how far each change spreads. Use content-visibility: auto when a page is long and mostly static, and most content starts offscreen: it skips rendering but keeps everything in the DOM, so accessibility, find-in-page and SEO are unaffected. Use virtualization when the number of items is very large (thousands of rows) or unbounded (infinite feeds), so that even holding them in the DOM costs too much memory and slows DOM queries and style invalidation.
They also combine well. A virtualized table can contain each row so cell updates stay local. A long article can use content-visibility for its sections and contain: content for an embedded interactive widget. A feed can apply content-visibility to pages of items and switch to virtualization once the session has loaded more than a few hundred entries.
| Situation | Best fit |
|---|---|
| Long static article or docs page | content-visibility: auto |
| Busy widget on screen (ticker, chat) | contain: content or strict |
| Thousands of list rows | Virtualization |
| Infinite feed | content-visibility per page, then virtualization |
Debugging Containment Problems
When containment changes how something looks, the cause is almost always one of three guarantees being false. If an element collapsed, size containment applied to an element whose size depends on its children — remove size or set an explicit size. If something is clipped, paint containment is cutting off overflow — move the overflowing UI to the top layer or drop paint. If a fixed-position child moved, layout or paint containment made the element its containing block — move the fixed element outside. DevTools shows containment in the Computed panel, so checking which value applies is the fastest first step.
Containment in Component Libraries
Design systems can make containment the default for components that are always independent, such as cards with fixed media areas, toasts and self-contained panels. Document the guarantee each component makes, so product teams know not to place overflowing popovers inside them, and expose an opt-out for unusual layouts. Centralising the decision in the library spreads the benefit across every page without each team having to rediscover it.
A Rollout Plan
- Profile the longest templates and identify section boundaries.
- Apply
content-visibility: autoto sections beyond the second screen, withautointrinsic sizes. - Add
contain: contentto frequently updating widgets and check for clipped overflow. - Run visual regression, scroll, find-in-page and anchor tests.
- Release to a share of traffic, compare LCP, INP and CLS by template.
- Expand to more templates once the patterns are validated.
Measuring in the Field
Containment changes are hard to see in aggregate metrics because they affect specific templates and interactions. Segment field data by template, and for INP, by interaction target, using the Long Animation Frames API to separate style and layout time from script time. A drop in styleAndLayoutStart-to-end duration for interactions on contained widgets is the most direct evidence that containment is working.
Common Pitfalls
- Applying content-visibility above the fold. No skipped work, only overhead.
- No intrinsic size. Sections collapse to zero height while skipped, causing scrollbar jumps and shifts.
- Paint containment on widgets with dropdowns. Menus and tooltips get clipped.
- Measuring skipped elements in scripts. Forces rendering and layout.
- Expecting memory savings. Elements remain in the DOM; use virtualization for memory.
FAQ
What does content-visibility: auto do?
It lets the browser skip rendering an element's contents (style, layout, paint) while the element is offscreen, rendering them when it approaches the viewport.
Is content-visibility the same as virtualization?
No. Virtualization removes offscreen elements from the DOM. content-visibility keeps them in the DOM but skips rendering them. It is simpler and more accessible, but does not reduce memory or DOM size.
Why does my scrollbar jump with content-visibility?
Because skipped sections use the placeholder size from contain-intrinsic-size. If the estimate is far from the real size, the page height changes as sections render. Use auto and realistic estimates.
Does contain: layout improve performance on its own?
It can, by limiting layout to the contained subtree when its contents change. The gain depends on how much of the page would otherwise be laid out.
Is containment safe for SEO?
Yes. Content remains in the DOM and is rendered for crawlers that need it. It does not hide content.
Can containment break layouts?
Yes, if the promise is false: size containment on elements whose size depends on content collapses them, and paint containment clips overflow. Apply it only where the guarantees hold.
What is contain: content?
Shorthand for contain: layout paint style. It is the most common choice for independent widgets, because it does not require a fixed size.
Does content-visibility help INP as well as LCP?
Mostly LCP and load-time blocking, because it reduces the initial rendering work. It can also help INP on long pages, since interactions that invalidate layout have fewer rendered elements to process when offscreen sections are skipped.
How do I test containment changes before shipping?
Combine a Performance panel comparison (layout time and layout roots) with visual regression screenshots of every affected template and state, plus manual checks of find-in-page, anchors and keyboard navigation.
Guides in This Topic
- Using content-visibility: auto on long pages — skip offscreen rendering safely.
- contain-intrinsic-size to stop scrollbar jumps — stable placeholders for skipped content.
- Isolating widgets with CSS contain — keep frequent updates local.
Related
- DOM size & list virtualization — when skipping rendering is not enough.
- Layout thrashing & forced reflow — scripts that defeat containment.
- Lazy-loading CSS background images — content-visibility also defers backgrounds.