Type something to search...
Understanding content-visibility for Faster Page Rendering

Understanding content-visibility for Faster Page Rendering

Long pages are expensive to render. A blog archive, a documentation page, a product listing, or a comment thread might contain thousands of elements, and by default the browser calculates style, layout, and paint for all of them before showing the first frame, even though the user can only see the top few hundred pixels.

The content-visibility property changes that. With a single declaration, you can tell the browser it's allowed to skip rendering work for sections that are offscreen, and to do that work only when they approach the viewport. On content-heavy pages the improvement in initial rendering time can be substantial. This guide explains how it works, how to size skipped content so the scrollbar doesn't jump, and where the property can surprise you.

What content-visibility Does

content-visibility controls whether an element renders its contents at all. It takes three values:

  • visible: the default. The element's contents render normally.
  • auto: the browser skips rendering the contents while the element is offscreen, and renders them when it gets close to the viewport.
  • hidden: the contents are never rendered, similar to display: none for the children, but the browser can keep their rendering state cached.

The value you'll use most is auto:

.article-section {
  content-visibility: auto;
}

When a .article-section is far from the viewport, the browser keeps the element's own box but skips style, layout, and paint for everything inside it. As the user scrolls and the section gets close to the visible area, the browser renders it just in time.

Importantly, skipped content is still in the DOM, still part of the accessibility tree, and still searchable with find-in-page. It's a rendering optimization, not a way of hiding content.

Why It Works: Containment

content-visibility: auto builds on CSS containment. When an element's contents are skipped, the browser applies layout, style, and paint containment to it, plus size containment while it's offscreen. Containment is a promise that nothing inside the element affects anything outside it:

  • Layout containment: internal layout can't change the position of outside elements.
  • Style containment: things like counters inside can't affect the rest of the page.
  • Paint containment: contents can't draw outside the element's box.
  • Size containment: the element's size doesn't depend on its contents.

That last one is the catch. If the element's size doesn't depend on its contents, what size does it get while its contents are skipped? Without extra information, the answer is zero height.

Fixing Height with contain-intrinsic-size

If skipped sections collapse to zero height, the page is much shorter than it should be. As the user scrolls, sections render, grow to their real height, and the scrollbar jumps around. That's a bad experience, and it can shift content.

The contain-intrinsic-size property gives the browser a placeholder size to use while contents are skipped:

.article-section {
  content-visibility: auto;
  contain-intrinsic-size: auto 800px;
}

This means: while this section is skipped, pretend it's 800px tall. The auto keyword adds something important. Once the section has been rendered once, the browser remembers its real size and uses that instead of the 800px estimate when it's skipped again. So the estimate only needs to be roughly right for sections the user hasn't reached yet.

contain-intrinsic-size is a shorthand. You can set each axis separately when needed:

.product-card {
  content-visibility: auto;
  contain-intrinsic-width: auto 280px;
  contain-intrinsic-height: auto 420px;
}

For block-level sections that fill their container's width, the width comes from the layout anyway, so you typically only care about the height. The logical versions contain-intrinsic-block-size and contain-intrinsic-inline-size also exist.

Choosing a Good Estimate

Pick a value close to the typical rendered height. Measure a few representative sections in DevTools and use something in the middle. If section heights vary wildly, consider applying content-visibility to smaller, more uniform units, such as individual cards or comments, rather than big regions.

A Practical Example: A Long Article Page

Here's a documentation page where each chapter is a <section>:

<main class="docs">
  <section class="docs-chapter" id="installation">
    <h2>Installation</h2>
    <p>...</p>
  </section>

  <section class="docs-chapter" id="configuration">
    <h2>Configuration</h2>
    <p>...</p>
  </section>

  <!-- dozens more chapters -->
</main>
.docs-chapter {
  content-visibility: auto;
  contain-intrinsic-size: auto 1200px;
}

/* The first chapter is always visible on load, so don't bother skipping it */
.docs-chapter:first-of-type {
  content-visibility: visible;
}

The browser now renders the first chapter immediately and skips the rest until the user scrolls toward them. Anchor links to #configuration still work, because the browser renders a skipped element when it needs to scroll it into view.

A Practical Example: A Card Grid

content-visibility also works well for grids of repeated items:

<ul class="product-grid">
  <li class="product-card">
    <img
      src="/images/lamp.webp"
      alt="Brass desk lamp"
      width="400"
      height="400"
      loading="lazy"
    />
    <h3>Brass Desk Lamp</h3>
    <p class="price">$89</p>
  </li>
  <!-- hundreds more -->
</ul>
.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 1.5rem;
}

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

For very long lists, virtualization libraries remove offscreen items from the DOM entirely and will be faster still. content-visibility is a lighter option: no JavaScript, no change to your markup, and the content stays accessible and searchable.

Using content-visibility: hidden

The hidden value never renders the element's contents, but unlike display: none, the browser can preserve the rendering state. That makes it useful for content you toggle frequently, like tab panels or a single-page app's inactive views:

.tab-panel:not(.is-active) {
  content-visibility: hidden;
}

Switching tabs back to a previously rendered panel can be faster than with display: none, because the browser doesn't have to rebuild everything from scratch.

There's an important accessibility difference, though. Content hidden with content-visibility: hidden is not rendered, so it isn't exposed to assistive technology and can't be found with find-in-page, much like display: none. Use it only for content that should genuinely be hidden from everyone.

Using hidden="until-found"

A related HTML feature is hidden="until-found". It hides content but lets find-in-page and fragment links reveal it, which is ideal for collapsed accordion sections:

<div class="faq-answer" hidden="until-found">
  You can cancel your subscription at any time from the billing page.
</div>
document.querySelectorAll(".faq-answer").forEach((panel) => {
  panel.addEventListener("beforematch", () => {
    panel.previousElementSibling?.setAttribute("aria-expanded", "true");
  });
});

When the browser finds a match inside, it fires beforematch, removes the hidden attribute, and scrolls to the text. Under the hood this uses content-visibility: hidden. Support for hidden="until-found" has been more limited than for content-visibility itself, so check current support and make sure your accordion still works without it.

Reacting When Content Becomes Visible

The browser fires a contentvisibilityautostatechange event on elements with content-visibility: auto when they start or stop skipping their contents. You can use it to start or pause expensive work, like canvas rendering or animations, only when a section is actually on screen:

const chart = document.querySelector(".sales-chart");

chart.addEventListener("contentvisibilityautostatechange", (event) => {
  if (event.skipped) {
    stopChartAnimation(chart);
  } else {
    startChartAnimation(chart);
  }
});

This event has narrower support than the property itself, so treat it as an enhancement. IntersectionObserver is the widely supported alternative.

Measuring the Impact

Always measure. The benefit depends on how much content is offscreen and how complex it is.

  1. Open the Performance panel in DevTools.
  2. Record a page load with and without content-visibility.
  3. Compare the time spent in Recalculate Style, Layout, and Paint during the initial render.

On a simple page with little offscreen content, the difference may be negligible. On a long page with complex sections, rendering time for the first frame can drop dramatically, which often improves metrics like Largest Contentful Paint and makes the page responsive sooner.

Pitfalls and Gotchas

Don't Skip Above-the-Fold Content

Applying content-visibility: auto to content that's visible on load gives no benefit and adds a small amount of overhead. Apply it to sections below the initial viewport.

Scrollbar Jumps

If contain-intrinsic-size is missing or far off, the scrollbar thumb will jump as sections render. Always pair content-visibility: auto with an estimated size.

Paint Containment Clips Overflow

Because paint containment is applied, anything that overflows the element's box gets clipped: dropdown menus, tooltips, decorative elements positioned outside the box, or large shadows. If a component relies on overflow, apply content-visibility to a wrapper that's big enough, or don't use it on that component.

Layout Queries Force Rendering

JavaScript that reads layout for skipped content, such as getBoundingClientRect() or offsetHeight on elements inside a skipped section, forces the browser to render it, which removes the benefit. Scroll-position logic and analytics scripts are common culprits.

Counters and Stacking Contexts

Style containment scopes CSS counters. If you number headings across the page with CSS counters, sections with content-visibility: auto may break the numbering. The element also gets its own stacking context, which can change how z-index behaves for its children.

Don't Apply It to Everything

It's tempting to add content-visibility: auto to every element. That creates overhead and increases the chance of hitting the side effects above. Use it on a moderate number of large, self-contained sections or repeated items.

Browser Support

content-visibility shipped first in Chromium-based browsers and is now supported in current versions of Firefox and Safari as well, with Safari the most recent to add it. Because it's a pure optimization, it degrades gracefully: browsers that don't understand it simply render everything as usual. You can use it today without a fallback.

If you want to apply the intrinsic size only where content-visibility is supported, wrap both in a feature query:

@supports (content-visibility: auto) {
  .docs-chapter {
    content-visibility: auto;
    contain-intrinsic-size: auto 1200px;
  }
}

Support for the related contentvisibilityautostatechange event and hidden="until-found" has lagged behind the property itself, so check current data on caniuse before relying on them.

Conclusion

content-visibility: auto is one of the rare performance features that's almost free to adopt. Apply it to large, self-contained sections below the fold, give each one a reasonable contain-intrinsic-size with the auto keyword, and the browser can skip rendering work for content the user hasn't scrolled to yet.

Watch for the side effects of containment, clipped overflow, scoped counters, and new stacking contexts, and don't apply it to content that's visible on load. Measure before and after in the Performance panel, and on long, content-heavy pages you'll likely see the first render get noticeably faster.

Tags :
Share :

Related Posts

A Complete Guide to CSS Container Queries

A Complete Guide to CSS Container Queries

For more than a decade, responsive design meant one thing: media queries. You asked the browser how wide the viewport was and adjusted your layout ac

Continue Reading
A Comprehensive Guide to Installing Next.js

A Comprehensive Guide to Installing Next.js

Next.js has emerged as a powerful framework for building React applications, offering features like server-side rendering, static site generation, an

Continue Reading
Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

CSS has evolved significantly, and modern tools like clamp(), min(), and max() are powerful game-changers in dynamic styling. If you’ve struggl

Continue Reading