What is content-visibility, and how does it improve rendering performance for long pages?
Advertisement
🧩 Scenario
Architecture Walkthrough
What It Skips
content-visibility: auto tells the browser it may skip rendering work for an element's contents while they are not relevant to the user, which in practice means off-screen and not otherwise needed.
The skipped work is style, layout, and paint for the subtree. The element itself still participates in layout, so it occupies space in the flow, but its descendants are not laid out or painted until they approach the viewport. As the user scrolls, subtrees are rendered just before becoming visible and can be skipped again afterwards.
The gain is largest on initial load, where a page with hundreds of items would otherwise lay out and paint all of them before the user sees the first screen. Reported improvements on long documents are often in the multiples for rendering time, which is unusual for a two-line CSS change.
It composes with the contain property: content-visibility: auto implies layout, style, and paint containment on the element while skipped, which is part of how the browser can safely skip the subtree.
The Three Values
visible is the default and skips nothing.
auto is the useful one: skip when off-screen, render when relevant. The subtree remains in the accessibility tree and remains findable, because browsers force rendering for find-in-page, anchor navigation, fragment links, and focus.
hidden skips the contents unconditionally and does not render them when they scroll into view. It is a manual mode where the developer controls visibility, and it differs from display: none in an important way: the rendering state is preserved, so revealing it again is cheap and does not rebuild the subtree from scratch. That makes it well suited to things like tab panels or accordion content that will be shown again, where display: none would discard the state and pay full cost on every reveal.
Note that hidden content is not findable and is removed from the accessibility tree, so anything using it must be genuinely hidden from the user's perspective, not merely off-screen.
contain-intrinsic-size Is Not Optional
Here is the failure everybody hits first. A skipped subtree has no computed height, so the element collapses to zero. The page becomes far shorter than it should be, the scrollbar is wrong, and as the user scrolls each subtree renders and expands, causing the scroll position to lurch and the scrollbar thumb to jump around.
contain-intrinsic-size supplies a placeholder size the browser uses while the subtree is skipped:
.row {
content-visibility: auto;
contain-intrinsic-size: auto 120px;
}
The auto keyword there is worth using: it tells the browser to remember the last rendered size and use that instead of the fallback once the element has been rendered, which makes the estimate progressively accurate as the user scrolls. Without auto, the fixed value is used permanently, and a poor estimate produces persistent scroll drift.
The estimate does not need to be exact, but it should be close to the typical item height. A wildly wrong estimate is worse than a slightly wrong one because the correction on render is larger.
Apply It Per Item, Not Once Around Everything
A common misapplication is putting content-visibility: auto on a single wrapper containing the whole list. That skips almost nothing, because the wrapper is on screen as soon as its top edge is, and once it is relevant its entire subtree renders.
The property belongs on the repeated units: each row, card, article, or section. Then each unit is skipped or rendered independently, which is where the win comes from.
It should also not be applied to above-the-fold content. Content that is visible immediately gains nothing from being skipped, and the containment it implies can prevent optimisations elsewhere. Applying it from the second or third item onward, or simply accepting that the first items render immediately because they are on screen, is the right shape.
Compared With Virtualisation
content-visibility and virtualisation solve overlapping problems differently, and the distinction matters for very large lists.
content-visibility: auto keeps every element in the DOM. It skips rendering work but not DOM construction, memory for the nodes, or event listener attachment. It requires no JavaScript, preserves find-in-page, keeps the accessibility tree intact, and works with any markup.
Virtualisation removes off-screen elements from the DOM entirely, so DOM size stays constant regardless of list length. That is the only approach that scales to tens of thousands of items, because DOM node count itself becomes the bottleneck. The cost is a JavaScript library, broken find-in-page, more complex accessibility handling, and scroll position management.
The practical rule: content-visibility for long documents and lists in the hundreds, virtualisation for lists in the thousands and above. They can also be combined, with virtualisation controlling DOM membership and content-visibility reducing work for the rendered window.
For images and iframes specifically, loading="lazy" is a separate and complementary tool that defers the network request rather than the rendering.
Measuring
The improvement shows up in the Performance panel as reduced layout and paint time during initial render, and in Lighthouse as improvements to Largest Contentful Paint and Total Blocking Time on long pages. The Rendering panel's paint flashing makes it visible directly: with the property applied, only the region near the viewport flashes on scroll.
Because the benefit depends entirely on how much off-screen content exists, it is worth measuring rather than assuming. On a short page it does nothing; on a thousand-item index it can be transformative.
Key Code Explained
/* THE PATTERN: apply to the REPEATED UNIT, not to one wrapper */
.article-list > .article-card {
content-visibility: auto;
contain-intrinsic-size: auto 180px; /* placeholder while skipped */
}
/* WRONG: one wrapper around everything skips almost nothing.
As soon as the wrapper is on screen, its whole subtree renders. */
.article-list {
content-visibility: auto;
contain-intrinsic-size: auto 4000px;
}
/* WITHOUT contain-intrinsic-size: skipped subtrees collapse to zero,
the page is far too short, and the scrollbar lurches on scroll */
.broken-row {
content-visibility: auto;
}
/* The auto keyword remembers the LAST RENDERED size, so the
estimate becomes accurate as the user scrolls */
.row {
content-visibility: auto;
contain-intrinsic-size: auto 120px;
}
/* Without auto, the fixed value is used permanently and a poor
estimate causes persistent scroll drift */
.row-fixed-estimate {
content-visibility: auto;
contain-intrinsic-size: 0 120px;
}
/* hidden: skips unconditionally, does NOT render on scroll,
but PRESERVES rendering state so revealing is cheap */
.tab-panel[hidden-panel] {
content-visibility: hidden;
}
/* display: none would discard the state and pay full cost on every reveal.
But hidden content is not findable and leaves the accessibility tree,
so use it only for genuinely hidden content. */
/* Do NOT apply it to above-the-fold content — nothing is saved */
.hero {
content-visibility: visible; /* the default; stated here for emphasis */
}
/* Complementary tools, different jobs */
.article-card img {
loading: lazy; /* defers the NETWORK request */
aspect-ratio: 16 / 9; /* reserves space, avoiding layout shift */
}
/* Containment alone, when skipping is not wanted but scope limiting is */
.widget {
contain: layout paint;
}
The two contrasting .article-list blocks are the most important part. The wrong version looks like the natural place to put the property, and it delivers almost no benefit, because containment and skipping operate on the element's contents and the wrapper becomes relevant as soon as its top edge enters view. Applying it per card is what allows hundreds of independent skip decisions.
The contain-intrinsic-size: auto 120px form is the other detail to carry. The auto keyword changes the property from a fixed guess into a self-correcting estimate that uses the real measured height once an element has been rendered, which is the difference between scroll behaviour that improves as the user moves down the page and scroll behaviour that stays wrong.
Tradeoffs
| Approach | DOM size | Skips layout/paint | Find-in-page works | Needs JavaScript | Scales to |
|---|---|---|---|---|---|
| Nothing | Full | No | Yes | No | Short pages |
content-visibility: auto | Full | Yes, off-screen | Yes | No | Hundreds of items |
content-visibility: hidden | Full | Yes, always | No | Manual control | Tab panels, accordions |
| Virtualisation | Constant | Yes, by removal | No | Yes | Tens of thousands |
| Both combined | Constant | Yes, twice over | No | Yes | Very large datasets |
loading="lazy" | Full | Defers network only | Yes | No | Image-heavy pages |
What Interviewers Actually Check
- Whether you can say precisely which work is skipped and which still happens
- Whether you know
contain-intrinsic-sizeis required and why - Whether you distinguish
hiddenfromdisplay: noneon state preservation - Whether you apply it to repeated units rather than a single wrapper
- Whether you can compare it honestly with virtualisation rather than presenting it as a replacement
Follow-Up Questions
- What does the
autokeyword incontain-intrinsic-sizechange, and what happens without it? - Which containment types does
content-visibility: autoimply, and why are they necessary for skipping to be safe? - Why does
content-visibility: hiddenpreserve rendering state whendisplay: nonedoes not, and what does that buy? - How do browsers handle find-in-page and anchor navigation into a skipped subtree?
- How would you combine
content-visibilitywith a virtualised list, and what would each layer be responsible for?
Common Candidate Mistakes
- Omitting
contain-intrinsic-size, so skipped subtrees collapse to zero height, the page is far too short, and the scrollbar lurches as content renders during scroll - Applying the property to a single wrapper around the whole list, which skips almost nothing because the wrapper becomes relevant as soon as its top edge enters the viewport
- Treating
content-visibility: hiddenas equivalent todisplay: none, missing that it preserves rendering state for cheap reveals but also removes the content from find-in-page and the accessibility tree - Applying it to above-the-fold content, which saves nothing since that content must render immediately and the implied containment can block other optimisations
- Presenting it as a replacement for virtualisation, when it keeps every node in the DOM and therefore does not address the DOM-size bottleneck for lists in the thousands
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you say which pipeline stages
content-visibility: autoskips, and what still happens? - Can you distinguish all three values, including how
hiddendiffers fromdisplay: none? - Can you explain the scrollbar problem and how
contain-intrinsic-size: auto <length>fixes it? - Can you compare it honestly with virtualisation on DOM size, searchability, and scale?
- Can you say where it should not be applied?
Summary
content-visibility: auto lets the browser skip style, layout, and paint for an element's contents while they are off-screen and otherwise irrelevant. The element itself still occupies space in the flow, but its descendants are not laid out or painted until they approach the viewport, and they can be skipped again afterwards. On long documents and lists that removes most of the rendering work from initial load, which is a large gain for a two-line CSS change with no JavaScript and no markup change.
Two implementation details are essential. contain-intrinsic-size must be supplied, because a skipped subtree has no computed height and the element would otherwise collapse to zero, producing a page that is far too short and a scrollbar that lurches as content renders during scroll. The auto keyword form, as in contain-intrinsic-size: auto 120px, tells the browser to remember the last rendered size, which makes the estimate self-correcting rather than a permanent guess. And the property belongs on the repeated units, each card or row, rather than on a single wrapper, since a wrapper becomes relevant as soon as its top edge enters view and then renders everything inside it.
The three values differ meaningfully: visible skips nothing, auto skips off-screen content while keeping it findable and in the accessibility tree, and hidden skips unconditionally while preserving rendering state, which makes reveals cheaper than display: none at the cost of removing the content from find-in-page and assistive technology. Compared with virtualisation, content-visibility keeps every node in the DOM, so it does not address DOM-size limits: use it for long pages and lists in the hundreds, use virtualisation for thousands and above, and combine them for very large datasets.
Why is contain-intrinsic-size necessary?
Because a skipped subtree has no computed height, so without an estimate the element collapses to zero. That makes the scrollbar jump as content is rendered on scroll.
Is content-visibility: auto content still findable with Ctrl+F?
Yes in modern browsers, which force rendering for find-in-page, anchor navigation, and accessibility tree access. content-visibility: hidden does not, which is the key difference from auto.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement