content-visibility: the new CSS property that boosts your rendering performance

Improve initial load time by skipping the rendering of offscreen content.

Jeremy Wagner
Jeremy Wagner
Vladimir Levin
Vladimir Levin

Published: August 5, 2020

The content-visibility property enables the user agent to skip an element's rendering work, including layout and painting, until it is needed. Because rendering is skipped, if a large portion of your content is off-screen, using the content-visibility property makes the initial user load much faster. It also allows for faster interactions with the on-screen content. Pretty neat.

Browser Support

  • Chrome: 85.
  • Edge: 85.
  • Firefox: 125.
  • Safari: 18.

Source

demo with figures representing network results
In our article demo, applying content-visibility: auto to chunked content areas gives a 7x rendering performance boost on initial load. Read on to learn more.

CSS Containment

The key and overarching goal of CSS containment is to enable rendering performance improvements of web content by providing predictable isolation of a DOM subtree from the rest of the page.

Basically a developer can tell a browser what parts of the page are encapsulated as a set of content, allowing the browsers to reason about the content without needing to consider state outside of the subtree. Knowing which bits of content (subtrees) contain isolated content means the browser can make optimization decisions for page rendering.

There are four types of CSS containment, each a potential value for the contain CSS property, which can be combined in a space-separated list of values:

  • size: Size containment on an element ensures that the element's box can be laid out without needing to examine its descendants. This means we can potentially skip layout of the descendants if all we need is the size of the element.
  • layout: Layout containment means that the descendants do not affect the external layout of other boxes on the page. This allows us to potentially skip layout of the descendants if all we want to do is lay out other boxes.
  • style: Style containment ensures that properties which can have effects on more than just its descendants don't escape the element (e.g. counters). This allows us to potentially skip style computation for the descendants if all we want is to compute styles on other elements.
  • paint: Paint containment ensures that the descendants of the containing box don't display outside its bounds. Nothing can visibly overflow the element, and if an element is off-screen or otherwise not visible, its descendants will also not be visible. This allows us to potentially skip painting the descendants if the element is offscreen.

Skip rendering work with content-visibility

It may be hard to figure out which containment values to use, since browser optimizations may only kick in when an appropriate set is specified. You can play around with the values to see what works best, or you can use content-visibility to apply the needed containment automatically. content-visibility ensures that you get the largest performance gains the browser can provide with minimal effort from you as a developer.

The content-visibility property accepts several values, but auto is the one that provides immediate performance improvements. An element that has content-visibility: auto gains layout, style and paint containment. If the element is off-screen (and not otherwise relevant to the user—relevant elements would be the ones that have focus or selection in their subtree), it also gains size containment (and it stops painting and hit-testing its contents).

What does this mean? In short, if the element is off-screen its descendants are not rendered. The browser determines the size of the element without considering any of its contents, and it stops there. Most of the rendering, such as styling and layout of the element's subtree are skipped.

As the element approaches the viewport, the browser no longer adds the size containment and starts painting and hit-testing the element's content. This enables the rendering work to be done just in time to be seen by the user.

A note on accessibility

One of the features of content-visibility: auto is that the off-screen content remains available in the document object model and therefore, the accessibility tree (unlike with visibility: hidden). This means, that content can be searched for on the page, and navigated to, without waiting for it to load or sacrificing rendering performance.

The flip-side of this, however, is that landmark elements with style features such as display: none or visibility: hidden will also appear in the accessibility tree when off-screen, since the browser won't render these styles until they enter the viewport. To prevent these from being visible in the accessibility tree, potentially causing clutter, be sure to also add aria-hidden="true".

Example: a travel blog

In this example, we baseline our travel blog on the right, and apply content-visibility: auto to chunked areas on the left. The results show rendering times going from 232ms to 30ms on initial page load.

A travel blog typically contains a set of stories with a few pictures, and some descriptive text. Here is what happens in a typical browser when it navigates to a travel blog:

  1. A part of the page is downloaded from the network, along with any needed resources.
  2. The browser styles and lays out all of the contents of the page, without considering if the content is visible to the user.
  3. The browser goes back to step 1 until all of the page and resources are downloaded.

In step 2, the browser processes all of the contents looking for things that may have changed. It updates the style and layout of any new elements, along with the elements that may have shifted as a result of new updates. This is rendering work. This takes time.

A screenshot of a travel blog.
An example of a travel blog. See Demo on Codepen

Now consider what happens if you put content-visibility: auto on each of the individual stories in the blog. The general loop is the same: the browser downloads and renders chunks of the page. However, the difference is in the amount of work that it does in step 2.

With content-visibility, it will style and layout all of the contents that are currently visible to the user (they are on-screen). However, when processing the story that is fully off-screen, the browser will skip the rendering work and only style and layout the element box itself.

The performance of loading this page would be as if it contained full on-screen stories and empty boxes for each of the off-screen stories. This performs much better, with expected reduction of 50% or more from the rendering cost of loading. In our example, we see a boost from a 232ms rendering time to a 30ms rendering time. That's a 7x performance boost.

What is the work that you need to do in order to reap these benefits? First, we chunk the content into sections:

An annotated screenshot of chunking content into sections with a CSS class.
Example of chunking content into sections with the story class applied, to receive content-visibility: auto. See Demo on Codepen

Then, we apply the following style rule to the sections:

.story {
  content-visibility: auto;
  contain-intrinsic-size: 1000px; /* Explained in the next section. */
}

Specify the natural size of an element with contain-intrinsic-size

In order to realize the potential benefits of content-visibility, the browser needs to apply size containment to ensure that the rendering results of contents don't affect the size of the element in any way. This means that the element will lay out as if it was empty. If the element does not have a height specified in a regular block layout, then it will be of 0 height.

This might not be ideal, since the size of the scrollbar will shift, being reliant on each story having a non-zero height.

Thankfully, CSS provides another property, contain-intrinsic-size, which effectively specifies the natural size of the element if the element is affected by size containment. In our example, we are setting it to 1000px as an estimate for the height and width of the sections.

This means it will lay out as if it had a single child of "intrinsic-size" dimensions, ensuring that your unsized divs still occupy space. contain-intrinsic-size acts as a placeholder size in lieu of rendered content.

The auto keyword for contain-intrinsic-size causes the browser will remember the last-rendered size, if any, and use that instead of the developer-provided placeholder size. For example, if you specified contain-intrinsic-size: auto 300px, the element will start out with a 300px intrinsic sizing in each dimension, but once the element's contents are rendered, it will retain the rendered intrinsic size. Any subsequent rendering size changes will also be remembered. In practice, this means that if you scroll an element with content-visibility: auto applied, and then scroll it back offscreen, it will automatically retain its ideal width and height, and not revert to the placeholder sizing. This feature is especially useful for infinite scrollers, which can now automatically improve sizing estimation over time as the user explores the page.

Hide content with content-visibility: hidden

What if you want to keep the content unrendered regardless of whether or not it is on-screen, while leveraging the benefits of cached rendering state? Enter: content-visibility: hidden.

The content-visibility: hidden property gives you all of the same benefits of unrendered content and cached rendering state as content-visibility: auto does off-screen. However, unlike with auto, it does not automatically start to render on-screen.

This gives you more control, allowing you to hide an element's contents and later unhide them quickly.

Compare it to other common ways of hiding element's contents:

  • display: none: hides the element and destroys its rendering state. This means unhiding the element is as expensive as rendering a new element with the same contents.
  • visibility: hidden: hides the element and keeps its rendering state. This doesn't truly remove the element from the document, as it (and it's subtree) still takes up geometric space on the page and can still be clicked on. It also updates the rendering state any time it is needed even when hidden.

content-visibility: hidden, on the other hand, hides the element while preserving its rendering state, so, if there are any changes that need to happen, they only happen when the element is shown again (i.e. the content-visibility: hidden property is removed).

Some great use cases for content-visibility: hidden are when implementing advanced virtual scrollers, and measuring layout. They're also great for single-page applications (SPA's). Inactive app views can be left in the DOM with content-visibility: hidden applied to prevent their display but maintain their cached state. This makes the view quick to render when it becomes active again.

Effects on Interaction to Next Paint (INP)

INP is a metric that evaluates a page's ability to be reliably responsive to user input. Responsiveness can be affected by any excessive amount of work that occurs on the main thread, including rendering work.

Whenever you can reduce rendering work on any given page, you're giving the main thread an opportunity to respond to user inputs more quickly. This includes rendering work, and using the content-visiblity CSS property where appropriate can reduce rendering work—especially during startup, when most rendering and layout work is done.

Reducing rendering work has a direct effect on INP. When users attempt to interact with a page that uses the content-visibility property properly to defer layout and rendering of offscreen elements, you're giving the main thread a chance to respond to critical user-visible work. This can improve your page's INP in some situations.

Conclusion

content-visibility and the CSS Containment Spec mean some exciting performance boosts are coming right to your CSS file. For more information on these properties, check out: