How List Rendering Can Cause Huge Cumulative Layout Shift

Do you…

  • care about Core Web Vitals, especially Cumulative Layout Shift (CLS)
  • use list rendering with a JS Framework (v-for, *ngFor, …)

If you answered both with yes, then please read on.

Slow connection device causes 0.51 CLS upon updating a list.

Slow connection device causes 0.51 CLS upon updating a list.

JS frameworks—such as Vue, Angular or React—can cache DOM nodes of list items*. Filtering a list will therefore be faster, because DOM nodes of list items can potentially be reused. This reuse is done by moving the DOM nodes around, instead of re-creating them.

But when a list changes and the nodes of list items merely switch their positions, the reused items are considered as a shift in the DOM (CLS) by Core Web Vitals on slow connection devices.

*) Vue uses :key, React uses key, Angular uses *ngFor with trackBy


Why only on slow connection devices?

CLS takes user interaction into account. When content is changing after a click, it’s not counted towards the CLS until a grace period of 500ms. If the delay of content changing takes longer than 500ms, you’ll be penalised with the full CLS score.

Fast connection devices will usually finish updating the list before the grace period ends.

Keep an eye out in your Google Search Console for URLs that are frequented by countries that are further away from your servers/CDNs and have a lower average mobile bandwidth.

A whopping 433k Indian URLs are affected here by list rendering on slow connection devices.

A whopping 433k Indian URLs are affected here by list rendering on slow connection devices.


Solution

Google’s calculation of CLS has been changed before, so I would love to see this here too. Until then it’s on us to not get penalised.

We’d have to make sure, that the DOM elements get re-created everytime a list gets updated or filtered. I wouldn’t advise to completely drop the unique key (:key). Rather think of a key for each list item that is tied to the request itself, e.g. :key="item.id + "-" + requestQuery". The filtering itself will have a brief flash due to recreating the DOM node, but considering the Green URLs that Google will give you it’s worth it.

With this you can continue with infinite scroll and page navigation without perf loss.


If you found this post interesting please leave a ❤️ on this tweet and consider following my 🎢 journey about #webperf, #buildinpublic and #frontend matters on Twitter.

Optimising for Core Web Vitals (4 part series)
1 Core Web Vitals explained with GIFs
2 How List Rendering Can Cause Huge Cumulative Layout Shift
3 Accurate Daily Measurements of Core Web Vitals with Google Analytics
4 The Duality of CLS with Lazy Loading Components
Simon Wicki

Simon Wicki is a Freelance Developer in Berlin. Worked on Web and Mobile apps at JustWatch. Fluent in Vue, Angular, React and Ionic. Passionate about Frontend, tech, web perf & non-fiction books.

Twitter Follow @zwacky