A failed experiment about CSS containment

Aubin De Traversay
Wantedly Engineering
3 min readMar 10, 2022

As I mentioned in my previous post Tips about improving SEO as a developer, the DX team at Wantedly recently worked on improving our SEO scores. Part of this effort was to improve our Page Experience metrics, which generally includes page load performance.

Since every new task is an occasion to learn something, and new features are constantly added to the web platform, I saw it as an opportunity to try a new CSS property from the CSS Containment Spec.

Introducing content-visibility

The content-visibility property is a feature introduced a little while ago that allows to specify that an element might not need to be rendered on screen immediately. Its typical use-case is to set it to off-screen elements so the browser doesn’t have to do work until the user scrolls down the page. For more info about how it works, you should consult this article on Web.dev.

A comparison of loading the same page without and with the new CSS property. It shows that the time spent by the browser to render the page goes from 232ms to 30ms
Illustration from web.dev as linked above. Creative Commons Attribution 4.0 License

My use-case was a page listing a potentially large number of items, and refactoring it for pagination was an effort we didn’t want to spend time on yet. Large pages with thousand of DOM nodes are very costly to render, to the point of making some crash. That case thus looked like an easy win with the content-visibility: auto;property: a few lines of CSS lead to a reducing the amount of time spent on layout work from 3 to 6 times on large lists! (This seems to correspond to the improvements indicated in the article linked above.) And with the magic of CSS, this is also a safe change as incompatible browsers will ignore the property.

The second step was then to set the contain-intrinsic-size to give the off-screen elements a placeholder size. This property is very important for the user experience, without it the scrollbar becomes a very unreliable indicator. Commit and ship, then go on to do other tasks.

The unexpected result

Early on, the results of this change were positive — albeit very small, but worth it for such a small change. But soon after, the page started to show up in our Search Console diagnostics as having a bad Cumulative Layout Shift (later CLS) score.

When using the Web Vitals diagnostic tool from the Chrome Dev Tools and scrolling up-and-down the page, the metric did increase despite no particular layout work being done due interactions.

A screenshot demonstrating the overlay in the top-right corner of a page and the value for each of 3 core web vitals. Largest Contentful Paint, First Input Delay, and Cumulative Layout Shift. The latter is showing a value of 0.22.
Chrome Devtool’s Rendering → Core Web Vitals overlay will show you in how the CLS evolves as you interact with a page

I had to check all moving parts around the page in the performance profiler to see what layout operations were at the source of this issue, but none seemed to pass this investigation. It’s only by eliminating all these suspects that I was left with my last change to the page: the addition of content-visibility and contain-intrinsic-size .

When adding these, I actually made a mistake: due to the complexities of responsive webpages, the target list elements had a height that would vary depending on the device. I assumed that contain-intrinsic-size was merely used for the scrollbar and that an approximation of the final size was good enough. However that caused the browser to re-adjust the size of each element coming into the viewport as the user scrolls! This constant layout recalculations were akin to constantly setting the height property via Javascript, a very expensive task, causing subtle but constant layout jank.

Shows a timeline from the Chrome Devtools indicating regular Layout shift sections in red.
Extract of the Chrome Dev Tools’ Performance panel when scrolling the page

Another solution I was looking forward to is to use the contain-intrinsic-size: auto $size; variant that makes the browser remember the size of previously computed elements, shipped in Chrome 98. But that only eliminates a part of the work as the browser still has to compute each element at least one, and my short experiment actually made the CLS worse.

The takeaway is: be careful with contain-intrinsic-size and learn how to measure your performance!

--

--