Skip to content

CSS Containment and content-visibility

advanced8 min read

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.

Mental Model

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: hidden but 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. */
}
Common Trap

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:

  1. Off-screen: The browser skips layout, paint, and compositing for the element's subtree entirely. It applies contain: layout style paint size automatically.
  2. Entering viewport: The browser renders the element normally as it scrolls into view.
  3. 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.

Execution Trace
Initial
Page has 50 article sections, each with images, code blocks, and text
Without containment: browser renders ALL 50 sections
With c-v
content-visibility: auto applied to each section
Browser only renders sections in/near viewport
Load
4 sections visible in viewport → only 4 sections are rendered
46 sections skip layout, paint, and compositing
Scroll
User scrolls down → section 5 enters viewport → browser renders it
Just-in-time rendering as content enters view
Scroll
Section 1 scrolls far above viewport → browser stops rendering it
Cached height (contain-intrinsic-size: auto) preserves scroll position
Result
At any point, only ~6 sections are fully rendered (viewport + buffer)
90% rendering savings on a 50-section page

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 doWhat 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
Quiz
A page has 100 article sections. You add content-visibility: auto to each. What is the approximate rendering cost at initial page load?
Quiz
You apply contain: layout to a card component. A child element inside the card changes height. What happens outside the card?
Key Rules
  1. 1contain: content (layout + paint) is the most useful containment shorthand — use it on repeated items like cards and list items.
  2. 2content-visibility: auto is the single most impactful CSS property for long pages — it skips rendering off-screen content entirely.
  3. 3Always pair content-visibility: auto with contain-intrinsic-size: auto <height> to prevent scrollbar instability.
  4. 4contain: size requires explicit dimensions — without them, the element collapses to 0x0.
  5. 5Containment creates layout boundaries that prevent style/layout changes from cascading to siblings and parents.
  6. 6content-visibility: auto can deliver 5-10x rendering performance improvements on content-heavy pages with zero JavaScript.