Performance
Cumulative Layout Shift
Learn how to measure, prevent, and improve layout stability on your website to enhance user experience and search engine ranking.
Definition of Cumulative Layout Shift (CLS).
Cumulative Layout Shift (CLS) is a critical web performance metric that assesses the visual stability of a webpage. It measures how much page content unexpectedly shifts during loading, directly impacting the user experience. Understanding CLS is essential for designers, as it helps create more visually consistent and user-friendly websites.
Cumulative Layout Shift (CLS) is a Core Web Vitals metric used to evaluate web page loading and interactivity. It quantifies the extent of unexpected layout shifts that occur as a user interacts with a page, impacting the visual stability and potentially causing a frustrating user experience.
CLS is calculated by summing the product of the impact fraction and the distance fraction of all layout shifts that happen within the user's viewport. The impact fraction represents the portion of the affected element, and the distance fraction accounts for how far the element moves within the viewport. This formula helps to assess the severity of layout shifts.
To prevent CLS, developers must ensure that all page elements have explicitly defined dimensions, especially images, and prioritize loading content that's above the fold. By optimizing images, utilizing lazy loading, and managing ads and embeds properly, developers can significantly reduce CLS and improve the overall user experience.
Unexpected movement of page content usually happens because resources are loaded asynchronously or DOM elements get dynamically added to the page above existing content. The culprit might be an image or video with unknown dimensions, a font that renders larger or smaller than its fallback, or a third-party ad or widget that dynamically resizes itself.
What makes this issue even more problematic is that how a site functions in development is often quite different from how users experience it. Personalized or third-party content often doesn't behave the same in development as it does in production, test images are often already in the developer's browser cache, and API calls that run locally are often so fast that the delay isn't noticeable.
The Cumulative Layout Shift (CLS) metric helps you address this problem by measuring how often it's occurring for real users.
What is CLS?
CLS is a measure of the largest burst of layout shift scores for every unexpected layout shift that occurs during the entire lifespan of a page.
A layout shift occurs any time a visible element changes its position from one rendered frame to the next. (See below for details on how individual layout shift scores are calculated.)
A burst of layout shifts, known as a session window, is when one or more individual layout shifts occur in rapid succession with less than 1-second in between each shift and a maximum of 5 seconds for the total window duration.
The largest burst is the session window with the maximum cumulative score of all layout shifts within that window.
What is a good CLS score?
To provide a good user experience, sites should strive to have a CLS score of 0.1 or less. To ensure you're hitting this target for most of your users, a good threshold to measure is the strong>75th percentile of page loads,segmented across mobile and desktop devices.
Layout shifts in detail
Layout shifts are defined by the
Layout Instability
API,
which reports layout-shift
entries any time an element that is visible within the viewport
changes its
start position (for example, its top and left position in the default
writing
mode) between
two frames. Such elements are considered unstable
elements.
Note that layout shifts only occur when existing elements change their start position. If a new element is added to the DOM or an existing element changes size, it doesn't count as a layout shift—as long as the change doesn't cause other visible elements to change their start position.
Layout shift score
To calculate the layout shift score, the browser looks at the viewport size and the movement of unstable elements in the viewport between two rendered frames. The layout shift score is a product of two measures of that movement: the impact fraction and the distance fraction (both defined below).
layout shift score = impact fraction * distance fraction
Impact fraction
The impact fraction measures how unstable elements impact the viewport area between two frames.
The union of the visible areas of all unstable elements for the previous frame and the current frame—as a fraction of the total area of the viewport—is the impact fraction for the current frame.
In the image above there's an element that takes up half of the viewport in one
frame. Then, in the next frame,
the element shifts down by 25%
of the viewport height. The red, dotted rectangle indicates the union of the element's visible
area in both frames, which, in this case,
is 75% of the total viewport, so its impact
fraction is 0.75
.
Distance fraction
The other part of the layout shift score equation measures the distance that unstable elements have moved, relative to the viewport. The distance fraction is the greatest distance any unstable element has moved in the frame (either horizontally or vertically) divided by the viewport's largest dimension (width or height, whichever is greater).
In the example above, the largest viewport dimension is the height, and the unstable element has moved by 25% of the viewport height, which makes the distance fraction 0.25.
So, in this example the impact fraction is 0.75
and the distance fraction is 0.25
,
so the layout shift score is 0.75 * 0.25 =
0.1875
.
The next example illustrates how adding content to an existing element affects the layout shift score:
The "Click Me!" button is appended to the bottom of the gray box with black text, which pushes the green box with white text down (and partially out of the viewport).
In this example, the gray box changes size, but its start position does not change so it's not an unstable element.
The "Click Me!" button was not previously in the DOM, so its start position doesn't change either.
The start position of the green box, however, does change,
but since it's been moved partially out of the viewport,
the invisible area is not considered when calculating the impact
fraction. The union of the visible areas for the green
box in both frames
(illustrated by the red, dotted rectangle)
is the same as the area of the green box in the first frame—50%
of the viewport.
The impact fraction is 0.5
.
The distance fraction is illustrated with the purple arrow.
The green box has moved down by about 14%
of the viewport so the distance fraction is
0.14
.
The layout shift score is 0.5 x 0.14 = 0.07
.
This last example illustrates multiple unstable elements:
In the first frame above there are four results of an API request for animals, sorted in alphabetical order. In the second frame, more results are added to the sorted list.
The first item in the list ("Cat") does not change its start position between frames, so it's stable. Similarly, the new items added to the list were not previously in the DOM, so their start positions don't change either. But the items labelled "Dog", "Horse", and "Zebra" all shift their start positions, making them unstable elements.
Again, the red, dotted rectangles represent the union of these three
unstable elements' before and after areas,
which in this case is around 38%
of the viewport's area (impact fraction
of 0.38
).
The arrows represent the distances that unstable elements have moved from
their starting positions. The "Zebra"
element, represented by the blue arrow,
has moved the most, by about 30% of the viewport height.
That makes the distance fraction in this example
0.3
.
The layout shift score is 0.38 x 0.3 = 0.1172
.
Expected vs. unexpected layout shifts
Not all layout shifts are bad. In fact, many dynamic web applications frequently change the start position of elements on the page.
User-initiated layout shifts
A layout shift is only bad if the user isn't expecting it. On the other hand, layout shifts that occur in response to user interactions (clicking a link, pressing a button, typing in a search box and similar) are generally fine, as long as the shift occurs close enough to the interaction that the relationship is clear to the user.
For example, if a user interaction triggers a network request that may take a while to complete, it's best to create some space right away and show a loading indicator to avoid an unpleasant layout shift when the request completes. If the user doesn't realize something is loading, or doesn't have a sense of when the resource will be ready, they may try to click something else while waiting—something that could move out from under them.
Layout shifts that occur within 500 milliseconds of user input will have the
hadRecentInput
flag set, so they can be excluded from calculations.
Animations and transitions
Animations and transitions, when done well, are a great way to update content on the page without surprising the user. Content that shifts abruptly and unexpectedly on the page almost always creates a bad user experience. But content that moves gradually and naturally from one position to the next can often help the user better understand what's going on, and guide them between state changes.
Be sure to respect prefers-reduced-motion
browser settings, as some site visitors can experience ill effects or attention issues from animation.
CSS
transform
property allows
you to animate elements without triggering layout shifts:
-
Instead of changing the
height
andwidth
properties, usetransform: scale()
. -
To move elements around, avoid changing the
top
,right
,bottom
, orleft
properties and usetransform: translate()
instead.
How to measure CLS
CLS can be measured in the lab or in the field, and it's available in the following tools:
Field tools
- Chrome User Experience Report
- PageSpeed Insights
- Search Console (Core Web Vitals report)
-
web-vitals
JavaScript library
Lab tools
Measure CLS in JavaScript
To measure CLS in JavaScript, you can use the
Layout Instability
API.
The following example shows how to create a
PerformanceObserver
that listens for unexpected layout-shift
entries, groups them into sessions,
and logs the maximum session value any time it changes.
let clsValue = 0;
let clsEntries = [];
let sessionValue = 0;
let sessionEntries = [];
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
// Only count layout shifts without recent user input.
if (!entry.hadRecentInput) {
const firstSessionEntry = sessionEntries[0];
const lastSessionEntry = sessionEntries[sessionEntries.length - 1];
// If the entry occurred less than 1 second after the previous entry and
// less than 5 seconds after the first entry in the session, include the
// entry in the current session. Otherwise, start a new session.
if (sessionValue
&&
entry.startTime - lastSessionEntry.startTime
< 1000
&&
entry.startTime - firstSessionEntry.startTime
< 5000) {
sessionValue += entry.value;
sessionEntries.push(entry);
} else {
sessionValue = entry.value;
sessionEntries = [entry];
}
// If the current session value is larger than the current CLS value,
// update CLS and the entries contributing to it.
if (sessionValue
> clsValue) {
clsValue = sessionValue;
clsEntries = sessionEntries;
// Log the updated value (and its entries) to the console.
console.log('CLS:', clsValue, clsEntries)
}
}
}
}).observe({type: 'layout-shift', buffered: true});
This code shows the basic way to calculate and log CLS. However, accurately measuring CLS in a way that matches what is measured in the Chrome User Experience Report (CrUX) is more complicated. See below for details:
In most cases, the current CLS value at the time the page is being unloaded is the final CLS value for that page, but there are a few important exceptions:
The following section lists the differences between what the API reports and how the metric is calculated.
Differences between the metric and the API
- If a page is loaded in the background, or if it's backgrounded prior to the browser painting any content, then it should not report any CLS value.
- If a page is restored from the back/forward cache, its CLS value should be reset to zero since users experience this as a distinct page visit.
-
The API does not report
layout-shift
entries for shifts that occur within iframes, but to properly measure CLS you should consider them. Sub-frames can use the API to report theirlayout-shift
entries to the parent frame for aggregation.
In addition to these exceptions, CLS has some added complexity due to the fact that it measures the entire lifespan of a page:
- Users might keep a tab open for a very long time—days, weeks, months. In fact, a user might never close a tab.
- On mobile operating systems, browsers typically do not run page unload callbacks for background tabs, making it difficult to report the "final" value.
To handle such cases, CLS should be reported any time a page is background—in addition to any time it's
unloaded
(the
visibilitychange
event covers both of these scenarios). And analytics systems receiving this data will
then need to calculate the final CLS value on the backend.
Rather than memorizing and grappling with all of these cases yourself,
developers can use the
web-vitals
JavaScript library
to measure CLS,
which accounts for everything mentioned above:
import {getCLS} from 'web-vitals';
// Measure and log CLS in all situations
// where it needs to be reported.
getCLS(console.log);
You can refer to
the source code
for getCLS)
for a complete example of how to measure CLS in JavaScript.
How to improve CLS
For most websites, you can avoid all unexpected layout shifts by sticking to a few guiding principles:
- Always include size attributes on your images and video elements, or otherwise reserve the required space with something like CSS aspect ratio boxes. This approach ensures that the browser can allocate the correct amount of space in the document while the image is loading. Note that you can also use the unsized-media feature policy to force this behavior in browsers that support feature policies.
- Never insert content above existing content, except in response to a user interaction. This ensures any layout shifts that occur are expected.
- Prefer transform animations to animations of properties that trigger layout changes. Animate transitions in a way that provides context and continuity from state to state.