Skip to content
Docs

Improve Cumulative Layout Shift (CLS) on Vercel

Read, diagnose, and fix Cumulative Layout Shift on Vercel using Speed Insights and Next.js best practices.

6 min read
Last updated July 8, 2026

Cumulative Layout Shift (CLS) measures how much a page's visible content moves around unexpectedly while it loads. It is one of Google's Core Web Vitals, and one of the metrics Vercel Speed Insights reports from your real users. You see it as a page that jumps: text reflows as a font swaps in, content drops when an image arrives, a button moves the instant before a user taps it.

This guide assumes you already ship on Vercel. It shows you how to read your CLS in Speed Insights, find the routes that shift, fix the common causes on a Next.js App Router app, and confirm the result with real-user data.

Target: field CLS at or below 0.1 at P75, verified in Speed Insights after deploy. (Speed Insights metrics)

Two routes render the same content with the same late-loading elements. The janky one reserves no space and shifts; the stable one reserves space and holds at CLS 0.

Janky: nothing reserves space, so each late element shoves the article down. The live CLS meter climbs to about 0.28.

Janky route_ the banner, promo, and image load and push the article down.gif

Janky route: the banner, promo, and image load and push the article down

Stable: the same elements arrive on the same schedule, but each slot holds its height, so nothing moves and CLS stays at 0.000.

Stable route_ content fills the reserved slots without moving anything.gif

Stable route: content fills the reserved slots without moving anything


CLS is the fraction of layout shift a user sees over the life of the page, summed over the worst burst of shifts (a "session window"). You rarely need the formula, only where you sit against the threshold. (Cumulative Layout Shift)

CLS valueRating
≤ 0.1Good
0.1 to 0.25Needs improvement
≥ 0.25Poor

Two things worth knowing before you investigate:

  • Shifts within 500 ms of a tap, click, or keystroke are expected and excluded. Opening a menu or accordion does not count against you. Only unexpected shifts accrue.
  • Speed Insights reports CLS at P75 and splits mobile from desktop. The two often differ because of viewport size, banners, and font load. Check both. (Dashboard view)

The usual causes are images without dimensions, web fonts that render at a different size than their fallback, and ads or widgets that resize themselves. (Cumulative Layout Shift)


You need field data before you can fix field CLS. Open the project's Speed Insights tab: charts mean it is collecting, an Enable button means it is not.

  • Not set up yet? Follow the Quickstart to enable it and add <SpeedInsights /> from @vercel/speed-insights/next. Speed Insights is free on Hobby for one project (10,000 events/month included); see limits and pricing for Pro and Enterprise.
  • From the CLI: an empty data array means nothing is being recorded.
vercel metrics vercel.speed_insights.cls -a p75 --since 7d --project your-project

Open Speed Insights, select CLS, and narrow down: (Dashboard view)

  1. Toggle mobile and desktop. Note which is worse.
  2. Use the Kanban board. It surfaces the routes, paths, and often the specific HTML element tied to poor scores, pointing you near what shifts. Routes under 0.5% of visits are hidden by default.
  3. Read the line graph with P90/P95 added. A step up usually lines up with a specific deploy.
  4. Check the map if a shift looks region-specific, such as a locale banner or consent UI.

Write down the route, device class, percentile, and current CLS. That is your baseline and your verification target.

Prefer route-level evidence over the project average. A project CLS of 0.08 can hide one template at 0.3.


Most CLS on a Next.js app traces to a few patterns. Inspect the flagged route for these, in rough order of how often they cause it.

An <img> with no dimensions has zero height until it loads, then pushes everything below it down. This is the most common cause.

  • Use next/image and set both width and height. Next.js reserves space from the aspect ratio so the image cannot shift as it loads. (next/image)
  • When the size is unknown (responsive or CMS images), use fill with a positioned parent, plus sizes and object-fit. (next/image → fill)

When the fallback font is replaced by the web font, the text reflows because the two fonts have different metrics. Use next/font: it self-hosts the font and generates a metrics-matched fallback via adjustFontFallback (on by default) to keep CLS near zero. Leave that default on. (Font optimization, next/font)

Anything that mounts after hydration and sits in the normal flow pushes content down. Reserve a fixed height (or min-height) for the slot before the content arrives, or pin overlays out of flow with position: fixed/absolute so they never displace anything.

Alerts, "you have N new items", or headers prepended after load shift everything below. Reserve the slot, or insert below the fold or as an overlay.

If a server-rendered shell differs in size from the hydrated result, the correction is a shift. Make skeletons and loading.tsx fallbacks the same dimensions as the final content, and reserve space for client-fetched sections.

Animating width, height, top, or margin moves neighbors. Use transform (translate, scale) instead, which does not trigger layout shift.


If you use a coding agent, this prompt does the investigation. It assumes the Vercel CLI is set up and tells the agent how to read CLS from the CLI itself.

Investigate why CLS is poor for <route> on this Vercel project. Start read-only;
do not edit files, change settings, or deploy without my approval.
1. Read the field data yourself:
vercel metrics vercel.speed_insights.cls -a p75 --group-by route \
-f "environment eq 'production'" --since 7d --project <project> --format json
Find routes above 0.1 at p75, then split the worst one by device_type.
2. Inspect the route's code for the usual causes: images without width/height,
next/font and adjustFontFallback, banners/ads/embeds and other UI that mounts
after load without reserved space, and skeletons that don't match final size.
3. Return the specific element and component, why it shifts, the smallest fix,
its risk, and how to verify with Speed Insights after deploy.

The metric is vercel.speed_insights.cls (query at -a p75; group by route or device_type). Run vercel metrics schema vercel.speed_insights.cls for the full dimension list. If you would rather the agent pull Vercel data live instead of via the CLI, connect Vercel MCP.


CLS is a field metric, so real confirmation comes from real users.

  1. Lab check first. In Chrome DevTools, reload the route with the Performance panel recording and confirm the element no longer moves. To name the exact node that shifted, run the Layout Instability API snippet below in the DevTools console. It is a diagnostic aid you run on local or a preview URL, not code you ship.
  2. Ship to preview, then production. Speed Insights tracks both. (Speed Insights overview)
  3. Watch the field data. Filter to the same route, device, and percentile you baselined and confirm CLS drops to ≤ 0.1. It is collected when users leave the page, so allow enough traffic before concluding. The line graph should step down at your deploy.
  4. Check for regressions on other routes that share the changed component.
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.hadRecentInput) continue; // ignore shifts within 500ms of input
for (const { node } of entry.sources ?? []) console.log(entry.value, node);
}
}).observe({ type: "layout-shift", buffered: true });

  • Fix a shared component (header, consent bar) once and verify across every route that uses it.
  • Watch CLS over time so a future deploy that reintroduces a shift shows up as a step change.
  • Catch regressions before production with a Deployment Checks integration that produces a Virtual Experience Score.

Was this helpful?

supported.

Read related documentation

No related documentation available.