JavaScript Layouts and Cumulative Layout Shift
 
Support Ukraine

JavaScript Layouts and Cumulative Layout Shift

On the first summer day of this year I surfed in on the Google Search Console[a] and saw that the God-machine in Sunnyvale had declared this humble blog to be unworthy.

The issue that plagued all my pages in the desktop version was a very high cumulative layout shift (CLS). That is, a lot of page elements were moving around while the page was loading, making it a poor experience for the user. Together with largest contentful paint and first input delay, cumulative layout shift make up the Core Web Vitals[b], a new set of user-centric metrics that Google rolled out to quantify the quality of websites and thus provide a signal to Google's ranking algorithm.

In particular, what caused Google to cast me beyond the pale was the masonry-style layouts that I have on many pages to display photos. Since neither HTML nor CSS have a built-in masonry layout, I've implemented it using JavaScript. But this causes a lot of content to shift during the page initialization - the photos are laid out in a single column by the browser, and then the JavaScript takes over and turns them into a masonry layout.

CLS doesn't apply when the movement of elements is caused by user input. But in order to combat elements jumping around on the page as it loads, loading the page is not considered to be "caused by user input", and this is what caused problems for my pages. The solution I ended up with had three components:

  1. Inline CSS styles: I added overflow-y: scroll to the body element in order to have a permanent scrollbar which makes the width of the page predictable.

  2. Fixed-size elements first: The fixed-width right sidebar on the page loads before the horizontally growing main article, but is laid out to the right of it by using a flexbox layout with reverse order.

  3. Inline load-time masonry layout: As each image loads, there is a snippet of inline JavaScript to move the element into the right position.

It was the last component - inline load-time layout - that took the longest to figure out. CLS is computed as amount of shift times area affected. What this means is that each shifted element affects itself and elements below it in the document flow. It also means that if an element is the last element in the flow, and it has size zero, it has no shift at all.

I ended up with two bits of JavaScript (and, of course, inlining the layout code): one just as the element is created, and one to finalize it:

<script>var layout = new LoadingMasonryLayout();</script>
<div id="new_div_0">
    <script>layout.beginElement(document.getElementById("new_div_0"));</script>
    ...
</div>
<script>layout.endElement(document.getElementById("new_div_0"));</script>

This way, each element could be moved into its approximately final position in the page as the page was loading. The beginElement() call would do the actual moving of the element, since it had to be done while the element didn't have any size. The endElement() call would be able to read the size of the fully-constructed element and therefore update the layout object so that the next masonry "brick" could be accurately positioned.

Update 2020-06-23: It seems like the God-machine has taken this blog back into its mercy.

2020-06-01, updated 2020-06-23