Improve Performances With Dynamic “Content-Visibility”

Using the power of the content-visibility CSS property on dynamic-sized elements

Initial Considerations

This article presents a few technologies that are only available on some browsers. This should not be considered as the solution you can use to solve all your performance issues but as the solution to improve some of your users’ experience.

Also, you should consider using it more as an experiment than an ultimate performance saver that can help you understand how a browser's rendering works and what can be controlled.

Finally, the code could also be improved by doing several enhancements — performance or features — to make it production ready. I use it on some websites, but it hasn’t been tested at scale.

The Content-Visibility CSS Property

When we have a big page with a huge amount of data, the browser will struggle with one of its steps to display the page because it asks for a lot of work.

The browser will go through the following step:

  • Loading

  • Scripting

  • Rendering

  • Painting

The bigger the page is, the slower these operations will be. But in this game, the rendering step is an Achilles’ heel for everyone looking for speed. The browser makes a lot of calculations here to prepare the painting, and the more the page is complex, the more time it takes.

Unlike a house where you can do the painting once the construction is complete — and maybe once in a while for a deserved refreshing — a browser has many reasons to make a lot of renderings over time. JavaScript DOM modifications, scrolls, dynamic carousels, and so on.

To help developers take more control over this, Chromium has introduced a new CSS property call content-visibility that aims to solve this performance by skipping this rendering part when it is not necessary, mainly because the concerned block is out of the screen, which, in our single page applications, can happen a lot: have you ever seen an e-commerce website without a vertical scroll?

This newly introduced property takes three possible values, which are visible, hidden and auto. As you can imagine, the first one, visible exist to tell the browser that a block should always be rendered, no matter if it is on-screen or not, while the hidden value always hides the element from rendering. Since we want to improve our performances, the last one is the one that is the most interesting for us: it will make the browser look at the supposed position of a given block and decide by itself if the block should be rendered or not.

How Does It Work?

Setting this CSS property to auto will prevent the browser from executing the rendering part of the displaying, saving a lot of execution time (rendering + painting) on all the hidden elements of the page. Then, as soon as the elements appear on the screen, the rendering will be made, and you can see your content properly.

However, if it hides the rendering, you should not be concerned about SEO issues or having accessibility consequences: all the document model -and therefore the accessibility tree- is available: web crawlers will not suffer from any issues, and the screen readers will work as expected. You should also remember that this CSS property is experimental, so the old-fashioned system will ignore it until it is correctly implemented.

Finally, once the element quits the screen, it is removed from your page rendering. It will then save another list of potential heavy calculations, such as if you have @keyframes animations, for example.

Setting the Block Size

As the content is not always rendered on your page, one thing might look a bit strange: your window height will not be the full-rendered one, so scrollbars won’t be able to do its proper height. With content removed from rendering as it leaves the page, this issue is not only a problem at the bottom but also at the top: the upper part of the screen is removed from the page height as you scroll it out of the screen.

So, to make sure the space is reserved, you can use another CSS property there reserve this very space when using the content-visibility CSS property: it is contain-intrinsic-size that you can use to set the width and height of the elements. content-intrinsic-size: 400px 300px will tell the browser that the associated block is 400px width and 300px height.

But…

It is very easy to use if you want to display a card as you have fixed dimensions. Still, sometimes, you might not know the size of the element you are not rendering, such as in a blog post: paragraphs have various sizes, and code blocks can also have differences in dimensions. And blog posts will likely be long and complicated. Hello, syntax highlighting on code blocks.

It is a shame that our performances won’t have the amazing benefit of this powerful feature.

Hopefully, a few other amazing APIs in the browser can come to the rescue!

The IntersectionObserver JavaScript object

The IntersectionObserver is an API that detects when a block gets visible or leaves the screen viewport, which is pretty much the same, but using content-visibility this time, it is JavaScript, so it gets more interactive and customizable.

It has a lot of awesome usages, such as loading the next or previous pages in an infinite loading page as soon as the footer gets into the viewport, pausing a video if you scroll it out of the screen, or lazy loading images -even tho you should use loading="lazy" instead.

How Does It Work?

To use an IntersectionObserver you will need three things:

  • A callback function will be executed as the element enters or leaves the screen's visible part. This is where your magic will happen.

  • An observer object that will handle the observation configuration, such as the sensibility of the detection. It also means that you can use the same configuration object for every block of your code you want to observe, reducing the impact on the memory.

  • An attachment to a DOM object that we will observe. In our scenario, it will be the block with a dynamic height on which we can’t use the content-visibility property.

Enough with the words. Here is a very simple code example of how to get started:

Deep Dive Into the Callback Method

As you saw in the previous gist, the callback method takes two parameters.

The first one is the list of intersections that have just occurred. It is an object with many parameters that might be used in different use cases, but we can focus on the two most important for today:

  • isIntersecting which is a boolean set to true appears according to the configuration false, otherwise. That’s the only information useful in our use case, as we want to know if the element has reached the viewport and thus has been rendered and now has a real height.

  • target which is the element that is currently intersecting. It will be the same as the DOM object we listened to, but since a single IntersectionObserver can be used many times, this property is more reliable.

The second parameter is the observer instance. It allows us to start listening to another element if we make an infinite loading system. Still, in our case, we can stop the observation as soon as we collect the height.

Now, let’s see how we can link it to the content-visibilityfeature.

Handle Blocks With Dynamic Heights

Linking IntersectionObserver and content-visibilty is now very simple: we have every tool we need, and we have to add the smart behavior in the callback method to make an overall improvement on our webpage:

  • First, we create an IntersectionObserver to detect visibility changes in a given block. When the detection is done, we will get into the callback method.

  • Inside the callback method, we can collect the given element's actual size since the browser has rendered it. It is as simple as const { height } = entry.target.getBoundingClientRect();

  • We can now set the CSS properties on the object. It is a one-liner: entry.target.style.containIntrasicSize = `${height}px` . There is no need to set the content-visibility: it should have already been assigned in your CSS to make this work. If you haven’t already, you will lose the benefit of delaying the rendering part.

  • Finally, since the height is now assigned, we don’t need the observation anymore, so we can stop the observation on this block

Here is a code example merging these two features:

Handling Responsiveness

There is one final touch we can bring to our code to make it more production ready: if the window gets resized, some blocks might have a changed height, and thus, our assigned value is not relevant anymore. And that’s the beauty of responsive designs.

To ensure the height is always the right one, we can just observe the resize window event, remove the assigned height and bring the observer back to business. As soon as the element crosses the visual part of the page, its height will be recalculated.

Conclusion

We are now ready to make our pages a lot faster! These two light APIs allow the developer to be more in control of what the page needs to display at any time. We can benefit from the power of content-visibility event on blocks with a dynamic height!

If you are working with frameworks like me, you can find a React hook and a Vue composable you can reuse in your applications at the end of this blog post.

Feel free to give me feedback on your Core Web Vitals improvements with this snippet!

React Hook

Vue Composable

Want to Connect?

If you want to learn more about JavaScript, React and Vue,
feel free to follow me on Twitter.

Did you find this article valuable?

Support Bruno Sabot's Blog by becoming a sponsor. Any amount is appreciated!