Cumulative layout shift (CLS)
Chrome 77+ Edge 79+ Firefox Safari Details
Quick check for Cumulative Layout Shift (opens in a new tab), a Core Web Vital that measures visual stability. CLS tracks how much the page layout shifts unexpectedly during its lifetime, providing a single score that represents the largest burst of unexpected layout shifts.
Why this matters
Unexpected layout shifts are frustrating and can cause users to click the wrong element or lose their reading position. CLS directly impacts user experience and is a ranking factor for Google Search. Common causes include images without dimensions, web fonts, and dynamically injected content.
CLS rating thresholds
| Rating | Score | Meaning |
|---|---|---|
| Good | ≤ 0.1 | Stable, minimal shifting |
| Needs Improvement | ≤ 0.25 | Noticeable shifting |
| Poor | > 0.25 | Significant layout instability |
Need to debug? Use Layout Shift Tracking to identify which elements are causing shifts.
Snippet
Run it from an AI agent: the prompt uses the CLI to measure any URL and report the result.
// CLS Quick Check
// https://webperf-snippets.net
(async () => {
if (!PerformanceObserver.supportedEntryTypes?.includes("layout-shift")) {
console.warn("⚠️ layout-shift entries are not supported in this browser.");
return { script: "CLS", status: "unsupported", error: "layout-shift entries not supported in this browser" };
}
// CLS is the largest session window: shifts less than 1s apart, in a window of at most 5s.
let cls = 0;
let sessionValue = 0;
let sessionFirst = null;
let sessionLast = null;
const addShift = (entry) => {
if (entry.hadRecentInput) return;
if (
sessionLast &&
entry.startTime - sessionLast.startTime < 1000 &&
entry.startTime - sessionFirst.startTime < 5000
) {
sessionValue += entry.value;
} else {
sessionValue = entry.value;
sessionFirst = entry;
}
sessionLast = entry;
cls = Math.max(cls, sessionValue);
};
const valueToRating = (score) =>
score <= 0.1 ? "good" : score <= 0.25 ? "needs-improvement" : "poor";
const RATING = {
good: { icon: "🟢", color: "#0CCE6A" },
"needs-improvement": { icon: "🟡", color: "#FFA400" },
poor: { icon: "🔴", color: "#FF4E42" },
};
const logCLS = () => {
const rating = valueToRating(cls);
const { icon, color } = RATING[rating];
console.log(
`%cCLS: ${icon} ${cls.toFixed(4)} (${rating})`,
`color: ${color}; font-weight: bold; font-size: 14px;`
);
};
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) addShift(entry);
logCLS();
});
observer.observe({ type: "layout-shift", buffered: true });
// Update on visibility change (final CLS)
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
observer.takeRecords();
console.log("%c📊 Final CLS (on page hide):", "font-weight: bold;");
logCLS();
}
});
// Expose function for manual check
window.getCLS = () => {
logCLS();
const rating = valueToRating(cls);
return {
script: "CLS",
status: "ok",
metric: "CLS",
value: Math.round(cls * 10000) / 10000,
unit: "score",
rating,
thresholds: { good: 0.1, needsImprovement: 0.25 },
};
};
console.log(
" Call %cgetCLS()%c anytime to check current value.",
"font-family: monospace; background: #f3f4f6; padding: 2px 4px;",
""
);
// Return for agent. Chrome only exposes layout-shift entries through a PerformanceObserver,
// so wait for the buffered observer above to deliver them.
await new Promise((resolve) => setTimeout(resolve, 100));
const clsSync = cls;
const clsRating = valueToRating(clsSync);
return {
script: "CLS",
status: "ok",
metric: "CLS",
value: Math.round(clsSync * 10000) / 10000,
unit: "score",
rating: clsRating,
thresholds: { good: 0.1, needsImprovement: 0.25 },
message: "CLS tracking active. Call getCLS() for updated value after page interactions.",
getDataFn: "getCLS",
};
})();
Understanding CLS
CLS groups unexpected layout shifts into session windows and reports the largest window. A window starts at a shift and continues while each new shift arrives less than 1 second after the previous one, up to 5 seconds in total. The score of a window is the sum of its shift scores:
shift score = impact fraction × distance fraction
CLS = max over session windows of Σ shift scores- Impact fraction: How much of the viewport was affected
- Distance fraction: How far elements moved
What’s excluded
- Shifts within 500ms of user input (
hadRecentInput: true) - Shifts caused by scroll anchoring
CLS calculation flow
CLS lifecycle
Common causes
| Cause | Solution |
|---|---|
| Images without dimensions | Add width and height attributes |
| Ads/embeds without space | Reserve space with CSS |
| Dynamic content injection | Reserve space or insert below fold |
| Web fonts (FOIT/FOUT) | Use font-display: optional or size-adjust |
Browser support
| Feature | Chrome | Edge | Firefox | Safari |
|---|---|---|---|---|
| LayoutShift (opens in a new tab) | 77 | 79 | ||
| PerformanceObserver (opens in a new tab) | 52 | 79 | 57 | 11 |
| All of the above | 77 | 79 |
Source: MDN browser compatibility data (opens in a new tab), version 8.1.4.
Further reading
- Cumulative Layout Shift (CLS) (opens in a new tab) | web.dev
- Optimize CLS (opens in a new tab) | web.dev
- Layout Shift Tracking | Detailed debugging snippet