Calculating 'contain-intrinsic-size' for 'content-visibility'

contain-intrinsic-size

CSS property content-visibility can be used to easily improve rendering performance, but it should be used in conjunction with contain-intrinsic-size to be practical.

If you have looked into this before, you might know that using value auto for property content-visibility tells the browser to only render a row if it's inside of your viewport.

Basically it makes it possible to perform lazy-rendering, just like lazy-loading images.

Property contain-intrinsic-size provides an estimated height for rows that are not yet rendered, because those would otherwise have no height while outside of your viewport.

There is a problem, however.. How are you supposed to estimate the height of a row? It would be way too much work to manually measure the height of every row. Again and again after updating the contents of rows, too.

Requirements

I can not provide a ready-to-use solution, because every website uses a different design system and tool to manage their content. That's why I provide you a way to implement lazy-rendering into your own design. These are the requirements for implementation:

  • Viewport-based design or a predictable container-based design. If a container-based design also uses viewport-based sizing and/or sizes that vary in-between breakpoints that determine the width of containers, that will make the height of rows unpredictable. It will make it useless to cache the height of rows at certain container-widths, since caching the height of rows at certain container-widths is the way to provide an estimated height of a row by using contain-intrinsic-size.
  • Frontend editor that allows:
    • Custom JavaScript to load when editing a page.
    • Saving custom values to rows before saving a page.

Process of calculating contain-intrinsic-size

JS - Actions just before saving a page (inside editor)

  1. Measure height of rows at different devices (viewport-based design) or at multiple container widths (for container-based designs).
  2. Measure viewport width (viewport-based design) or container width (container-based).
  3. Save height of rows at certain viewport widths (for viewport-based designs) or save height of row at certain container-widths (for container-based designs).

PHP - Formulas for contain-intrinsic-size (outside editor)

  1. Query all rows with those HTML attributes, using PHP's built-in XML/HTML parser.
  2. Insert the values as CSS variables (inside style attribute).

CSS - Apply lazy-rendering, using the formulas (outside editor)

Query rows with the CSS variables. Apply content-visibility: auto.

If you use a viewport-based design

Apply CSS variables to calculate contain-intrinsic-size for current viewport-width.

If you use a container-based design

Apply CSS variables to set value for contain-intrinsic-size at current container-width.

Performance of homepage after lazy-rendering

The following results are based on 20 tests, 5 without and 5 with lazy-rendering, for a desktop device and a mobile device ( (5 + 5) * 2 ). The desktop device is nothing crazy, but I'd say it's powerful enough to provide consistent results. The mobile device is an iPhone X, emulated in Google Chrome DevTools on that same desktop device.

Desktop - No lazy-rendering

Loading23ms
Scripting104.6ms
Rendering760.4ms
Painting62ms
System116.8ms

Desktop - With lazy-rendering

Loading26.8ms+16.52%
Scripting105.6ms+0.96%
Rendering580.8ms-23.62%
Painting68ms+9.68%
System93ms-20.38%

Mobile - No lazy-rendering

Loading22.2ms
Scripting118.4ms
Rendering866ms
Painting15.2ms
System100.6ms

Mobile - With lazy-rendering

Loading22.6ms+1.80%
Scripting102.2ms-13.68%
Rendering591.4ms-31.71%
Painting14ms-7.89%
System90ms-10.54%

Is it worth it, doing all of this?

If you are up for a challenge.. it's worth it. Is it overkill? Yes. If you're not here for the challenge, then I suggest not to waste your time for such a minor improvement.

If you are up for the challenge or interested in how I managed to do so, please consider reading about how I managed to automically calculate contain-intrinsic-size.

You might stumble across some issues, that I faced as well.


If you are familiar with LiteSpeed Cache (a caching/performance plugin for WordPress) or any other solution to speed up your website, then you might know that it is possible to provide an image placeholder that shows whenever an image is not yet loaded.

It prevents the layout from shifting when an image loads it's dimensions change from 0x0 to, for instance, 500x800 in pixels. Which is annoying, by the way.

If you want to estimate the height of a container, you would have to measure the height at multiple screen sizes (and possibly screen heights, may your design use units based on viewport height).

That takes way too much time and effort.

How I managed to calculate the height

The main reason why this was possible in the first place, is the way the design of this website works. The design of this website adapts to 3 device categories:

  1. Mobile (max-width: 480px)
  2. Tablet (min-width: 481px) and (max-width: 1024px)
  3. Laptop & Desktop (min-width: 1025px)

Font-sizes and spaces (paddings and margins) are all stored in CSS variables. I designed this website in Adobe XD at a certain canvas size that I've then set as CSS variables to use in formulas (--designedForWidth and --designedForHeight).

Instead of storing all the different font-sizes and spaces as plain vw values, I decided to use a size in pixels in the formulas in combination with the canvas width (viewport width) for which the size is supposed to be.

The advantage is that you only have to design for the three device categories, because all font-sizes will scale up and down between device categories / media queries. Along with multipliers at mobile screens and at tablet screens for font size and for spacing, because scaling from desktop all the way down to mobile would make everything way too small.

Calculating contain-intrinsic-size

As the sizes scale linearly, I decided to make sure that the page builder, that I've built for our website, saves the width of the viewport and the height of every single row by adding those values as HTML attributes (attributes for viewport height and attributes for the container's height). It does this for all three devices: desktop, tablet, and mobile.

This results in six attributes:

  1. data-cache-vw-d (viewport width on desktop)
  2. data-cache-h-d (height of row on desktop)
  3. data-cache-vw-t (viewport width on tablet)
  4. data-cache-h-t (height of row on tablet)
  5. data-cache-vw-m (viewport width on mobile)
  6. data-cache-h-m (height of row on mobile)

Using PHP's built-in HTML parser I was able to seek for the containers and add CSS variables as an inline style to be used for calculating contain-intrinsic-size:

--cisM: calc((var(--vwUnitless) * 100) / 547.59375 * 4760.5px);
--cisT: calc((var(--vwUnitless) * 100) / 730.125 * 1154.59375px);
--cisD: calc((var(--vwUnitless) * 100) / 1920 * 1720.4271240234375px);

Variable --vwUnitless is equal to 1vw, except it does not carry the pixels unit. This is required to make the formula work, because CSS doesn't allow you to divide values with units (like pixels) with eachother. I had to use JavaScript to retrieve the innerHeight of window and paste it inline as a CSS variable declaration for document.documentElement.

What the formulas do is they scale the cached height of the row by dividing the current viewport width by the viewport width at the time of saving the page (with my page builder) and multiplying that by the height of the row at the time of saving the page.

In order to apply the lazy-rendering, I decided it's best to only apply it to the containers that have contain-intrinsic-size specified. The CSS is placed inline in the top of <head>, because it is critical CSS.

  @media (max-width: 480px) {
    body:not(.tw-builder) #wrapper:not(.disable-lazy-render) main#content .tw-builder-content > .row[style*='--cisM:'],
    body:not(.tw-builder) #wrapper:not(.disable-lazy-render) footer#tw-footer .tw-builder-content > .row[style*='--cisM'] {
      content-visibility: auto;
      contain-intrinsic-size: var(--cisM);
    }
  }

  @media (min-width: 481px) and (max-width: 1024px) {
    body:not(.tw-builder) #wrapper:not(.disable-lazy-render) main#content .tw-builder-content > .row[style*='--cisT:'],
    body:not(.tw-builder) #wrapper:not(.disable-lazy-render) footer#tw-footer .tw-builder-content > .row[style*='--cisT'] {
      content-visibility: auto;
      contain-intrinsic-size: var(--cisT);
    }
  }

  @media (min-width: 1025px) {
    body:not(.tw-builder) #wrapper:not(.disable-lazy-render) main#content .tw-builder-content > .row[style*='--cisD:'],
    body:not(.tw-builder) #wrapper:not(.disable-lazy-render) footer#tw-footer .tw-builder-content > .row[style*='--cisD'] {
      content-visibility: auto;
      contain-intrinsic-size: var(--cisD);
    }
  }

Using content-visibility: auto; while jumping to anchors or by specific amounts does not work properly, so I will apply the class disable-lazy-render to the body whenever the user wants to jump to a particular anchor, or when they use keyboard shortcuts like Page up, Page down, Home, and End.

Accuracy of estimation

The estimations are accurate enough so that users will not notice the height of the scrollbar-thumb increasing and decreasing sometimes as different containers get rendered (thus taking up space according to the rendered height).

Just try it out for yourself by scrolling through my homepage.

If you pay very close attention then you might notice, but it's not spectacular.

Good to know

Things that are good to know before implementing lazy-rendering in designs.

content-visibility: auto; behaves like overflow: hidden;

On this website I tend to make images bleed out of a certain width. I noticed that images that tried to reach beyond the boundaries, were cut off.

I managed to fix that by making the containers full-width and shaping the content-width by applying padding-left and padding-right instead of the default margin-left: auto;, margin-right: auto; and a declared width.

  body > .row {
    padding-left: calc((100% - var(--contentWidth)) / 2);
    padding-right: calc((100% - var(--contentWidth)) / 2);
  }

The padding basically allows elements to overflow the content-box and enter the padding-box without leaving the box model as a whole and getting cut off.

Rendered heights reset after scrolling out of range

After rendering a container that holds content-visibility: auto;, the rendered height will reset after scrolling out of range. That means that it will fall back to the height you've provided for contain-intrinsic-size.

If that causes problems for your design, you can always try to compare the clientHeight or the height retreived through element.getBoundingClientRect().height to the height provided for contain-intrinsic-size and apply a class if it appears to be rendered. You could then prevent containers with that class from being lazy-rendered over and over again (by scrolling back and forth into view).


I hope this inspired you to come up with your own solution for calculating contain-intrinsic-size, or to try an approach similar to mine — a design system that linearly scales sizes and spacings throughout three types of devices: desktop, tablet, and mobile.

>