CSS contain: Render Isolation for Performance
Tell browsers 'this element is independent' with CSS contain. Layout, paint, and style calculations get scoped to the subtree—potentially cutting layout costs by 80%+.
Change one element's width, and the browser recalculates layout for the entire page.
The contain property tells browsers "this element's internals won't affect
anything outside"—unlocking significant performance gains by limiting recalculation scope.
The Problem: Global Recalculation
Every DOM change potentially triggers style recalculation, layout (reflow), and paint for the entire document. When you update a single div's content, the browser must verify that nothing else shifted as a result—even elements completely unrelated to your change.
// Adding text to one element...
document.querySelector('.widget').textContent = 'Updated!';
// ...might cause:
// 1. Style recalc for ancestors (inheritance check)
// 2. Layout for siblings (did anything shift?)
// 3. Paint for unrelated elements (composite layers)
// 4. Counter/list recalculation throughout documentCSS Containment: Scope Boundaries
The contain property creates optimization boundaries. It's a promise to
the browser: "I guarantee this element is independent—skip checking the rest."
.widget {
contain: layout; /* Layout changes won't escape */
}
.card {
contain: paint; /* Paints are clipped to this element */
}
.isolated {
contain: strict; /* Maximum isolation */
}Containment Types
Layout Containment
.widget {
contain: layout;
}What it does: Internal layout changes won't affect external elements. The element becomes a containing block for absolutely positioned descendants and establishes an independent formatting context.
- Creates a new stacking context
- Becomes a containing block for
position: absolutechildren - Establishes independent formatting context (like
display: flow-root) - Internal float/margin changes don't affect siblings
/* Without layout containment */
.sidebar {
float: left;
}
.main {
/* Layout depends on .sidebar's width */
}
/* With layout containment */
.sidebar {
contain: layout;
float: left;
}
.main {
/* Browser knows .sidebar won't affect .main's layout */
}Paint Containment
.widget {
contain: paint;
}What it does: Content is clipped to the element's bounds. Descendants can't paint outside this box, enabling paint optimizations.
- Acts like
overflow: hiddenfor painting (but not scrolling) - Creates a new stacking context
- Creates a containing block for positioned elements
- Off-screen elements can skip paint entirely
/* Useful for virtualized lists */
.list-item {
contain: paint;
/* Browser can skip painting items outside viewport */
}Style Containment
.widget {
contain: style;
}What it does: Counter and quote styles are scoped to this subtree. Less commonly used, but important for components using CSS counters.
/* Counters are scoped */
.component {
contain: style;
counter-reset: items;
}
.component li::before {
counter-increment: items;
content: counter(items) ". ";
/* Won't affect or be affected by counters outside */
}Size Containment
.widget {
contain: size;
width: 200px;
height: 150px;
}What it does: Element's size is determined without looking at children. Requires explicit sizing—without it, the element collapses to 0×0.
- Danger: Element ignores children for sizing completely
- Must set explicit
widthandheight - Useful when size is known ahead of time (fixed cards, ads)
/* ❌ BAD: Element collapses to 0×0 */
.card {
contain: size;
/* No explicit size = invisible! */
}
/* ✅ GOOD: Explicit dimensions */
.card {
contain: size;
width: 300px;
height: 200px;
}Shorthand Values
/* content = layout + paint + style */
.widget {
contain: content;
}
/* strict = layout + paint + style + size */
.widget {
contain: strict;
width: 300px; /* Required with size! */
height: 200px;
}
contain: content is the safest "set and forget" value—it provides most
benefits without requiring explicit sizing.
Real Performance Impact
Containment shines with many independent components updating simultaneously:
/* 100 widgets updating every second */
.dashboard-widget {
contain: content;
}
/* Before: Each update → full document layout check
After: Each update → only widget subtree checked
On a dashboard with 100 widgets:
- Without contain: ~15ms layout per update
- With contain: ~2ms layout per update */When It Helps Most
- Virtualized lists: Items entering/leaving view
- Dashboards: Multiple widgets updating independently
- Chat/feed UIs: Messages being added frequently
- Animations: Elements that animate without affecting siblings
- Third-party embeds: Isolate unpredictable content
Gotchas and Edge Cases
1. Overflow and Paint Containment
/* Paint containment clips content! */
.container {
contain: paint;
}
.tooltip {
position: absolute;
top: -50px; /* This will be clipped! */
}
Paint containment acts like overflow: clip. Absolutely positioned descendants
that escape the element's bounds will be clipped.
2. Size Containment and Flexbox/Grid
/* Size containment breaks intrinsic sizing */
.flex-item {
contain: size; /* flex-grow/shrink won't work as expected */
flex: 1; /* Ignored—size is fixed */
}3. Stacking Context Side Effect
/* Both layout and paint create stacking contexts */
.parent {
contain: layout; /* Now a stacking context! */
}
.child {
z-index: 9999; /* Trapped in parent's context */
}
If you're debugging z-index issues,
check all stacking context triggers—contain is often overlooked.
content-visibility: The Auto Pilot
content-visibility builds on containment to automatically skip rendering
for off-screen content:
.list-item {
content-visibility: auto;
contain-intrinsic-size: 0 50px; /* Estimated height */
}
/* Browser will:
1. Apply contain: layout style paint to off-screen items
2. Skip layout/paint for items outside viewport
3. Use contain-intrinsic-size as placeholder height */For long lists, this can reduce initial render time by 90%+. But beware: content-visibility has accessibility and scrollbar gotchas.
Browser Support
contain has excellent support: Chrome 52+, Firefox 69+, Safari 15.4+, Edge 79+.
It's safe to use as progressive enhancement—browsers that don't support it simply ignore it.
/* Safe to use everywhere */
.widget {
contain: content;
/* Unsupported browsers: no effect (harmless)
Supported browsers: performance boost */
}Practical Recommendations
- Default choice:
contain: contentfor independent components - Virtualized lists:
content-visibility: autowithcontain-intrinsic-size - Fixed-size elements:
contain: strictwith explicit dimensions - Measure first: Use DevTools Performance panel to verify improvement
- Don't over-apply: Adding to every element creates overhead; target bottlenecks
Key Takeaways
containcreates optimization boundaries—browsers skip recalculating outside the elementcontain: contentis the safest all-around choice (layout + paint + style)contain: sizerequires explicit dimensions or element collapses- Paint containment clips overflow like
overflow: clip - Both
layoutandpaintcontainment create stacking contexts - Use
content-visibility: autofor automatic off-screen optimization
Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
Tools & Utilities
Related Insights
Explore related edge cases and patterns
Advertisement