CSS Containment and content-visibility
Teaching the Browser to Skip Work
By default, the browser assumes any change could affect anything — changing one element's font-size might change its height, which changes its parent, which shifts siblings, which triggers repaint across the entire page. This conservative approach is correct but expensive.
CSS containment lets you make promises to the browser about what won't happen, so it can safely skip work.
A browser without containment is like a librarian who re-alphabetizes the entire library every time one book is returned. CSS contain is a label on each bookshelf saying "changes to this shelf don't affect other shelves." Now when a book is returned, the librarian only re-sorts that one shelf. content-visibility: auto goes further — it's a curtain over distant shelves. "Don't even look at this shelf until someone walks over to it."
CSS contain Property
The contain property takes one or more containment types:
contain: layout
You promise: "Layout changes inside this element will never affect anything outside."
The browser can: Skip recalculating layout for the rest of the page when something inside changes.
.card-grid-item {
contain: layout;
/* If a card's content changes height, siblings and parents
are NOT recalculated. The card's overflow is clipped to
its own bounds. */
}
What this means in practice:
- The element becomes a layout boundary — its children's geometry changes don't propagate upward
- The element establishes a new formatting context (like
display: flow-root) - Positioned descendants are contained within it (acts as a containing block for absolutely positioned children)
contain: paint
You promise: "Nothing inside this element will ever paint outside its bounds."
The browser can: Skip repainting this element when off-screen, and skip compositing overflow.
.widget {
contain: paint;
/* Box-shadows, text, and child elements that overflow
are clipped to this element's bounds.
If the element scrolls off-screen, it's not painted at all. */
}
What this means in practice:
- Acts as a clipping boundary (similar to
overflow: hiddenbut without scrollbar implications) - Off-screen elements with paint containment may skip rasterization entirely
- Creates a new stacking context and formatting context
contain: size
You promise: "This element's size is independent of its children."
The browser can: Determine this element's size without laying out its children. But you must set an explicit size — otherwise the element collapses to 0x0.
.fixed-widget {
contain: size;
width: 300px;
height: 200px;
/* Children don't affect the widget's size.
Even if you add more content, the widget stays 300x200. */
}
contain: size without an explicit width and height collapses the element to 0x0 — its children no longer contribute to its size calculation. Always pair contain: size with explicit dimensions. If you want size containment only on the block axis (height), use contain: inline-size instead, which only constrains the inline (width) direction, letting height still depend on content.
contain: style
You promise: "CSS counters and quotes inside this element don't affect outside counters."
This is the narrowest containment type and rarely needed in practice. It prevents counter increments inside the contained element from being visible to sibling counter calculations.
Shorthand Values
/* contain: content = layout + paint (most useful shorthand) */
.article { contain: content; }
/* contain: strict = layout + paint + size (requires explicit dimensions) */
.fixed-panel { contain: strict; width: 400px; height: 600px; }
contain: content is the most commonly useful value — it provides layout and paint containment without requiring explicit dimensions.
content-visibility: auto
This is the most impactful single CSS property for rendering performance on long pages. It tells the browser: "Don't render this element until it's near the viewport."
.article-section {
content-visibility: auto;
contain-intrinsic-size: auto 500px; /* Estimated height for scrollbar */
}
When an element has content-visibility: auto:
- Off-screen: The browser skips layout, paint, and compositing for the element's subtree entirely. It applies
contain: layout style paint sizeautomatically. - Entering viewport: The browser renders the element normally as it scrolls into view.
- Leaving viewport: The browser stops rendering it again, caching the last-known size for scroll position stability.
contain-intrinsic-size: The Scroll Position Saver
Without a height estimate, elements with content-visibility: auto collapse to 0 height when off-screen, making the scrollbar jump erratically. contain-intrinsic-size provides an estimated height:
.section {
content-visibility: auto;
/* Fixed estimate — use when sections are roughly the same height */
contain-intrinsic-size: auto 600px;
/* The 'auto' keyword tells the browser: remember the actual rendered
height once computed, and use that instead of 600px for subsequent
skip-rendering cycles. */
}
The auto keyword before the size is important — it tells the browser to remember the actual rendered height after the element has been displayed at least once, preventing scrollbar jitter on repeat visits.
content-visibility rendering savings in numbers
Chrome's engineering team measured the impact on a long-form article page with 300 sections. With content-visibility: auto on each section:
- Initial rendering time dropped by 7x (from 350ms to 50ms)
- Off-screen sections consumed zero rendering budget
- Memory usage for off-screen paint layers dropped to near zero
- The only cost: the browser maintains a lightweight layout placeholder for each hidden section
The gains are proportional to how much content is off-screen. For a page where 90% of content is below the fold, you save roughly 90% of initial rendering work.
Combining Containment for Maximum Performance
/* Long-form content page — each section is isolated */
.content-section {
content-visibility: auto;
contain-intrinsic-size: auto 400px;
}
/* Dashboard widgets — fixed dimensions, fully contained */
.dashboard-widget {
contain: strict;
width: 300px;
height: 200px;
}
/* Card grid items — layout/paint contained, size determined by content */
.card {
contain: content; /* layout + paint */
}
/* Sidebar — contained from main content layout */
.sidebar {
contain: layout;
}
Production Scenario: The 10-Second Blog
You're going to love this one. A documentation site with 80+ sections per page took 10 seconds for initial render on mid-range Android phones. The browser was dutifully rendering all 80 complex sections (with syntax-highlighted code blocks, diagrams, and tables) on initial load, even though only 3 were visible.
The fix? Two CSS properties:
.doc-section {
content-visibility: auto;
contain-intrinsic-size: auto 500px;
}
Results:
- Initial render time: 10s → 1.2s (8x faster)
- Largest Contentful Paint (LCP): 8s → 1.5s
- Time to Interactive: 12s → 2s
- No JavaScript changes. No lazy loading logic. Two CSS properties.
The scrollbar was initially slightly inaccurate (estimated heights vs actual), but the auto keyword in contain-intrinsic-size corrected it after first render of each section.
| What developers do | What they should do |
|---|---|
| Use contain: size without setting explicit width/height Size containment means children don't contribute to the element's size. Without explicit dimensions, it collapses to 0x0. | Always pair contain: size with explicit dimensions, or use contain: content instead |
| Apply content-visibility: auto without contain-intrinsic-size Without a size estimate, off-screen elements collapse to 0 height, causing scrollbar jumping and layout instability | Always pair content-visibility: auto with contain-intrinsic-size: auto <estimated-height> |
| Use content-visibility: hidden for off-screen content auto automatically manages rendering based on viewport proximity. hidden requires manual toggling. | Use content-visibility: auto for scroll-based optimization. hidden is for manually controlled visibility (tab panels, etc.) |
| Apply contain: strict to fluid-height elements contain: strict includes size containment, requiring explicit dimensions. Content-dependent heights need contain: content instead. | Use contain: content for elements whose height depends on their content |
- 1contain: content (layout + paint) is the most useful containment shorthand — use it on repeated items like cards and list items.
- 2content-visibility: auto is the single most impactful CSS property for long pages — it skips rendering off-screen content entirely.
- 3Always pair content-visibility: auto with contain-intrinsic-size: auto
<height>to prevent scrollbar instability. - 4contain: size requires explicit dimensions — without them, the element collapses to 0x0.
- 5Containment creates layout boundaries that prevent style/layout changes from cascading to siblings and parents.
- 6content-visibility: auto can deliver 5-10x rendering performance improvements on content-heavy pages with zero JavaScript.