Home / Articles / Skipping Offscreen Rendering With content-visibility and contain-intrinsic-size

This article is published in English.

Skipping Offscreen Rendering With content-visibility and contain-intrinsic-size

Learn how content-visibility: auto cuts layout cost on long server-rendered pages, why contain-intrinsic-size is mandatory, and how it compares with virtualization.

2789 words

Long pages full of repeating items, such as product listings, feeds, archives and big tables, often feel slow even when their JavaScript is small. The reason is usually that the browser computes styles and layout for every item on the page, including hundreds of cards the visitor has not scrolled to and may never reach. This guide shows how two CSS properties, content-visibility and contain-intrinsic-size, let the browser postpone that work, how the change relates to Core Web Vitals and indexing, and where the technique breaks or is the wrong tool.

A typical case: a long listing that is slow for no obvious reason

Picture a "View all products" category page for an online store. Roughly 600 product cards, each with an image, a title, a price and a rating, are rendered on the server into one long page. There is no pagination, no infinite scroll and no virtualization. The business likes it this way: a single URL, every product browsable, everything reachable with the browser's find-in-page.

On a mid-range laptop the page takes close to four seconds before it feels interactive. The natural suspect is JavaScript: an oversized bundle, a misbehaving useEffect, a component re-rendering in a loop. A careful look at the bundle turns up nothing that explains the delay.

The Performance panel in DevTools tells a different story. Most of the main-thread time goes to Layout, and it happens before any script has a chance to matter.

Why offscreen content still costs you

Rendering is not a single operation. The browser first resolves styles for every element, then runs layout to determine each box's size and position, and only then paints pixels. Painting is largely confined to what is in or near the viewport, but style resolution and layout run for the whole document, including content far below the fold. By the time the browser decides what to paint, the expensive geometry work is already finished.

In the store example, that means all 600 cards, each with an image box, a wrapping title, a price and a rating row, pass through style and layout before the shopper sees the first one. The shopper sees perhaps eight products. The other 592 are not useful yet, but the browser has no way of knowing whether the visitor will ever scroll down, so by default it treats all of them as content that must be ready now. That is the waste worth eliminating. If you want a refresher on how these pipeline stages differ in cost, see our breakdown of what reflow, repaint and composite each cost the browser.

The fix: two declarations on the repeating element

Apply both properties to the element that repeats, here the product card. The first tells the browser it may skip rendering the card's contents while the card is far from the viewport. The second provides a placeholder size for cards whose real size is not known yet.

.product-card {
  content-visibility: auto;
  contain-intrinsic-size: auto 340px;
}

With content-visibility: auto, a card that is not close to the viewport is left in the DOM and in the accessibility tree, and find-in-page still locates its text. What changes is that the browser does not spend style, layout and paint effort on its contents until the card approaches the screen. Under the hood the property applies layout, style and paint containment to the element, which is what allows the browser to treat the subtree as independent and skip it safely.

How big the gains can be

In a demo published on web.dev, Google took a lengthy page divided into sections and brought it down from 232 ms of rendering to 30 ms, roughly a sevenfold improvement. Keep in mind that this is a purpose-built demo page rather than a production site. On a real product listing like the one described above, an improvement of around 2x is a more realistic expectation, which can still be the difference between a page that is still loading and a page that looks ready on first paint.

Extreme documents show the ceiling. In a Chrome Dev Summit talk, the property was applied to an enormous single-page HTML specification with more than 270,000 DOM nodes, and layout time fell from around 50 seconds to about 400 milliseconds. Few pages look like that, but it illustrates how much effort goes into content nobody can see.

It helps to be precise about what is happening. CSS does not make the processor faster. You are telling the browser that a large part of the work does not have to happen right now. When a page holds 600 cards and the viewport shows eight, rendering every card up front is rarely the best use of the main thread.

Why contain-intrinsic-size is not optional

Ship content-visibility: auto alone and the scrollbar starts to jump while you scroll, which is easy to mistake for an unrelated bug.

The cause is simple. A card whose layout has been skipped has no known height, so it sizes it as if it were empty. Across hundreds of skipped cards the total document height is badly wrong, and the scrollbar reflects that wrong height. As cards come into view and get their real size, the document grows and the thumb shifts.

contain-intrinsic-size supplies the size to assume while an element is skipped. The 340px in the example is an estimate for cards that have not been rendered. Precision is not required; a reasonable approximation keeps the scrollbar from lurching visibly.

What the auto keyword adds

The auto keyword is easy to overlook and matters a great deal. With auto, once a card has been rendered, the browser records its actual size. If the card later leaves the viewport and is skipped again, the browser uses that remembered size instead of your estimate. Without auto, every card falls back to the fixed estimate each time it scrolls out of view.

Cards on screen always display at their true height either way. The difference shows up with variable-height content: a long product name that wraps onto an extra line, or a sale badge that adds a row. Without auto, those cards snap back to the estimate when they leave the viewport and the scroll position jumps. Use auto every time.

Browser support and progressive enhancement

Support is no longer a Chrome-only story. Chrome has supported content-visibility since version 85 (2020), Firefox since 125, and Safari since version 18. At the time of writing it is listed as Baseline Newly Available, a status reached on September 15, 2025, meaning it works in all three major browser engines; check current compatibility data if you support older versions.

Browsers that do not understand the property simply ignore it and render everything as they always have. That makes it a progressive enhancement with no downside for older clients.

How it connects to Core Web Vitals and SEO

The motivation here is performance, but category pages are exactly the kind of URL search teams watch closely. They are public, they target valuable queries, and Google measures how they perform for real users.

Layout and paint work feed into two of the three Core Web Vitals that Google uses as a ranking signal:

  • Interaction to Next Paint (INP) benefits directly. Skipping render work for offscreen cards frees the main thread, so taps and clicks get a response sooner.
  • Largest Contentful Paint (LCP) benefits more indirectly. On long pages, less layout before the first paint usually lets the large visible content appear earlier.

Plenty of non-commerce pages share this shape, from documentation and news feeds to archives of blog posts, lengthy discussion threads and multi-section articles. Wherever many similar items are stacked vertically and most start offscreen, and the page is public, this change affects metrics Google measures.

Be careful about promising ranking improvements. A single site observed over a few weeks proves very little, and ranking depends on far more than one metric. What you can reasonably expect is movement in the right direction in field data, for example in PageSpeed Insights, once enough real-user samples accumulate.

The benefit is not limited to public pages. Dashboards, admin tables and internal tools suffer from the same 600-row rendering cost. Behind a login there is no search upside, but the user experience gain is the same, from the same CSS.

Does skipping rendering hide content from crawlers?

This is the first question a careful SEO specialist will ask, and it is reasonable given how many lazy-loading schemes have made content invisible to bots. The key distinction is that content-visibility: auto is a rendering optimization, not a visibility change. The content exists in the HTML and in the DOM from the moment the page loads; only layout and paint are deferred until the element nears the viewport.

Googlebot does not scroll the way a person does. Instead it uses an extremely tall viewport for rendering and then inspects the DOM it ends up with. Cards that are skipped but present are part of that DOM, so every product title and price is indexed like any other content.

Contrast that with the older pattern of inserting content only when a scroll event fires. Crawlers do not generate scroll events, so such content really could be missing from the index. content-visibility cannot cause this problem, because nothing is inserted later; it is all there from the start.

One caveat is unrelated to the property itself. If the page builds its content with client-side JavaScript, you have a separate SEO problem. Googlebot does execute JavaScript, but rendering is queued, slower and less reliable, and many other crawlers handle JavaScript poorly. For public content, render the HTML on the server and add content-visibility in CSS on top. That combination gives you crawlable content and better vitals.

Compared with infinite scroll, virtualization and custom observers

Infinite scroll, paginated APIs and virtualization all target the same slow-long-page problem, so it is worth comparing them honestly.

Load-on-scroll and paginated APIs

Loading more items as the user scrolls solves the problem at the data layer. It is the right choice when the dataset is truly huge and should never be sent to the browser in full.

It has costs. You need API changes, loading states, scroll listeners and state tracking for what has already been fetched. The experience changes as well: find-in-page cannot locate items that have not loaded, and reaching the end of the list becomes tedious. On a public category page there is an extra SEO burden, because products that only appear after scrolling do not exist for crawlers; you need paginated fallback URLs and additional markup to keep them indexable. For 600 cards that are already in the HTML, this means a rewrite plus new SEO work to fix what is really a rendering problem.

Virtualization libraries

Virtualization, with libraries such as react-window or TanStack Virtual, holds the full dataset in memory while dropping DOM nodes for anything outside the visible window. It works, and at very large scales it is the better choice, as discussed below.

The price is a JavaScript dependency, a component rewrite and notoriously fiddly handling of variable-height rows. Because removed items are not in the DOM at all, find-in-page cannot see them, screen readers struggle with them, and on a public page crawlers miss them.

A hand-rolled IntersectionObserver approach

Rendering items yourself when an IntersectionObserver reports them as visible means rebuilding in main-thread JavaScript what content-visibility: auto already does, and reintroducing scroll-anchoring bugs that the browser has already solved natively. There is little reason to write it today.

Choosing between them

The real argument for content-visibility is not that it beats these techniques but that it is far cheaper. It is one CSS property: no JavaScript, no API changes, no rewrite, and content stays in the DOM for search, assistive technology and find-in-page.

The trade-off must be stated clearly. content-visibility saves rendering work, not memory. Every DOM node still exists. At 600 cards that is negligible. At 50,000 or 100,000 items the size of the DOM becomes a problem in its own right, and virtualization earns its complexity. Identify which of the two problems you have, render cost or DOM size, before picking the tool.

Where it works and where it quietly breaks things

This is not a property to sprinkle everywhere. It pays off most where the same structure repeats many times, for example:

  • cards in a catalog or listing
  • posts in a social or news feed
  • rows of a large data table
  • replies under an article
  • sections of a long docs page
  • entries on a results page

Anything that is one of many similar blocks stacked vertically, with most of them offscreen at load, is a good candidate to test.

Measuring skipped content returns wrong numbers

The property conflicts with code that needs accurate geometry from a subtree before that subtree has been rendered. A typical example is reading the height of an inner element of a row that is still offscreen.

const height = row
  .querySelector('.details')
  .getBoundingClientRect()
  .height;

Measuring the inner content of a skipped element with getBoundingClientRect() before it has ever been rendered yields zero or otherwise incorrect values. The element's own box reports the placeholder size from contain-intrinsic-size, which may not match reality either. Code like this is common in real interfaces, for example when you:

  • anchor a tooltip next to a trigger
  • work out start and end values for an animation
  • decide where a dropdown menu opens
  • size rows for a virtual list
  • drive sticky positioning logic
  • line one component up against another

If exact geometry matters before the content is visible, test thoroughly before adding the property.

Other poor candidates

  • Sticky headers, plus layouts whose calculations only work when all children have real geometry at the same moment.
  • Anything above the fold. That content has to render right away regardless, so the property brings no benefit and costs a bit of extra bookkeeping.
  • Elements whose visual effects extend beyond their box. Because the property applies paint containment, overflowing content such as large shadows or popovers positioned inside the card can be clipped at the card's edge.

Two lesser-known details

The hidden value

content-visibility: hidden skips rendering much like display: none, but the browser keeps the element's rendering state cached. Showing it again is considerably cheaper than revealing a display: none element, because the work does not have to be redone from scratch. That makes it useful for tabs, offscreen menus and virtual scrollers. Unlike auto, content under hidden is not reachable by find-in-page while hidden.

Reacting to skip state changes

Whenever an element using content-visibility: auto switches between skipped and rendered, the browser dispatches a contentvisibilityautostatechange event. Listening for it lets you suspend expensive scripts, such as canvas drawing, for content the browser is not rendering anyway.

Verifying the effect on your own pages

A quick experiment makes the difference obvious. Build a test page with around 1,000 cards and a toggle that adds or removes content-visibility: auto. Open DevTools, go to the Performance panel, record a reload with the toggle off, then record again with it on, and compare the purple Layout blocks in the two traces.

Then apply the same check to your longest production page. Capture a trace while it loads. When the Layout portion dominates and interactivity arrives late, adding content-visibility: auto to the repeating elements is often the cheapest real improvement available: one property, no rewrite, no framework migration.

Key takeaways

  • Browsers run style and layout for the entire document; content-visibility: auto lets them defer that work for elements far from the viewport without removing anything from the DOM.
  • Always pair it with contain-intrinsic-size: auto <estimate> in the same change, or the scrollbar will jump.
  • Content stays indexable, searchable and accessible, which sets it apart from load-on-scroll and virtualization.
  • It saves render time, not memory; once the DOM itself becomes too large, virtualization is the right answer.
  • Avoid it above the fold, on elements whose geometry you measure before display, and on components that paint outside their own box.