Web Platform · Rendering Performance
CSS Containment and content-visibility: Skipping Rendering Work
What contain: size, layout, style, and paint each guarantee, how content-visibility skips work for offscreen subtrees, and the scroll and accessibility edge cases.
Containment is a way for a stylesheet to tell the engine something it would otherwise have to discover: that a subtree’s internals cannot affect anything outside it. The engine’s invalidation logic already tries to find such a boundary by inspecting properties. An explicit declaration removes the guesswork, and in the case of content-visibility it enables skipping an entire class of work — style, layout, and paint — for content that is nowhere near the screen.
Four kinds of containment, four different promises
contain accepts a space-separated list of keywords, and each one asserts something narrower than the last:
contain: layoutstates that the element’s internal layout cannot affect the outside. Boxes inside do not influence the element’s external position; floats and margins do not escape; the element becomes a containing block for absolutely and fixed positioned descendants. The element itself can still be sized by its contents, so the promise is about what happens outside, not about skipping the inside.contain: sizestates that the element’s size does not depend on its contents. This is the declaration that actually permits skipping layout of the subtree: if the size is independent, the children need not be laid out to determine it. The consequence is brutal and intentional — without an explicit size, the element collapses, because the engine takes the content-derived size to be zero in both axes.contain: stylescopes counters and, in the level 2 definition, other style effects such as quotes, so that they cannot affect elements outside the container.contain: paintclips descendants to the element’s padding box and prevents them from painting outside it, and it establishes the element as a stacking context and a containing block for fixed-position descendants.
contain: strict computes to size layout style paint, and contain: content to layout paint style — the combination that does not require an explicit size. The distinction between content and strict is the difference between “isolate me” and “isolate me and do not measure my contents”: strict on an auto-sized element produces a zero-height box.
/* Isolates internals from the page; still sized by its contents. */
.card {
contain: content;
border: 1px solid #d0d7de;
}
/* Skips layout of the subtree, so the size must be supplied. */
.thumbnail {
contain: size;
width: 200px;
aspect-ratio: 16 / 9;
}
Size containment and the placeholder problem
Once an element declares contain: size, the engine does not need its contents to lay out the parent. That is the mechanism content-visibility: auto uses to avoid laying out offscreen sections: the section is treated as a fixed-size placeholder while it is skipped.
The placeholder size comes from contain-intrinsic-size, which supplies a fallback size in each axis. Two forms exist: a fixed length pair, and the auto keyword prefix, which instructs the engine to remember the element’s last rendered size and reuse it as the placeholder. The auto form is what makes long lists stable: the first time a section is rendered, its real size is recorded, and subsequent skips use that recorded size instead of the estimate.
.feed-section {
content-visibility: auto;
contain-intrinsic-size: auto 600px;
}
Without contain-intrinsic-size, a skipped element contributes zero height, so the scrollbar jumps as content comes into view and the total scroll length is wrong. The auto keyword does not eliminate the problem on first render, because there is no previous size to reuse; an estimate close to the real average is still needed.
What content-visibility skips, and what it keeps
content-visibility: auto makes an element’s contents eligible for skipping when they are offscreen. The specification is precise about what happens in the skipped state:
- the element’s own box is still laid out, with the size from
contain-intrinsic-sizeif the contents are size-contained; - the contents are not laid out, painted, or hit-tested;
- the contents remain in the DOM and in the accessibility tree, and remain reachable by find-in-page; a match scrolls into view, which forces the subtree to render;
- style containment applies, so counters and quotes inside do not affect the outside.
content-visibility: hidden is different: the contents are skipped regardless of position, and the element keeps its box. It is closer to a suspend than to a removal: display: none drops the subtree’s layout entirely and rebuilds it when the value changes back, while a hidden element may reuse the styling and layout work it did before the skip.
The rendering-work benefit of auto is concentrated on pages with many independent sections: a document with hundreds of article bodies, or a dashboard with dozens of offscreen panels. It does little for a page whose entire content fits within a viewport or two, and nothing for a page whose cost is a single expensive visible element.
Edges where skipping is observable
Skipping is not transparent, and several behaviours change when content is offscreen.
Layout reads. A script that measures a skipped element gets the placeholder size, not the real size. getBoundingClientRect() on a child of skipped content has no layout to report. Code that measures offscreen content must force it into a rendered state, or must accept the intrinsic size.
Observers. A ResizeObserver on content inside a skipped subtree has only the placeholder to report, so its notification arrives when the content becomes relevant and is laid out again, not when the contents would have changed size. IntersectionObserver still reports the element’s intersection, because intersection is computed from the placeholder box, which is what makes lazy rendering around content-visibility workable.
Scroll anchoring. Because the placeholder can differ from the real size, the browser’s scroll anchoring has to adjust. The scroll anchoring specification exists precisely to keep the visual position stable when content above the viewport changes size, and content-visibility is one of the mechanisms that makes such changes routine. Without anchoring, a section that renders above the viewport would shift everything below it.
Find-in-page and fragment navigation. A URL fragment pointing into skipped content requires the engine to render the target before scrolling to it. If the intrinsic size is far from the real size, the scroll position after navigation can differ from where it would have been with everything rendered.
Printing. Print output must include all content, so a print stylesheet that leaves content-visibility: auto in place forces every section to render during pagination. This is not wrong, but it means the print layout cost is the full document’s, regardless of what the screen path skips.
Choosing between containment, visibility, and virtualization
Three techniques address the same problem — rendering work for content that is not needed — and they trade off differently.
Containment (contain: layout paint) bounds invalidation without skipping work. It is safe for any element with a definite size and has no observable effect on content. Its benefit is proportional to how often the page invalidates globally.
content-visibility: auto skips work for offscreen subtrees while keeping them in the document. It preserves semantics, links, find-in-page, and focusability at the cost of placeholder-size approximation and a first-render cost when a section scrolls into view. It is the least invasive way to avoid laying out a long document.
Virtualization removes offscreen content from the DOM entirely and renders it on demand. It gives exact control over how many items exist at once, which matters when the item count is in the tens of thousands, but it changes semantics: find-in-page cannot reach unrendered items, accessibility trees change as content appears, and focus can be lost when the focused element is recycled. The engineering cost is high and belongs to the cases where the DOM count itself, not just the layout work, is the problem.
The ordering is deliberate: containment first, content-visibility where long offscreen regions exist, virtualization only when the other two are insufficient. Measuring which one applies requires the frame-level attribution covered in Tracing and Attributing Rendering Work, and the scroll interaction — which is where placeholder sizes become visible — is the subject of Scrolling and the Compositor Thread.