ESC

Type to search the knowledge base.

Layout Instability API CLS

LayoutShift entries, value calculation, hadRecentInput, and how to attribute CLS in the field.

advanced3 min read
  • browser
  • cls
  • layout-instability
  • web-vitals
  • performance

CLS (Cumulative Layout Shift) measures unexpected movement of visible content. The Layout Instability API exposes layout-shift performance entries so RUM tools can attribute shifts to nodes and timing — not just a Lighthouse score.

Docs: Layout Instability API, web.dev CLS, Optimize CLS.

What a layout shift entry contains

const po = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      console.log(entry.value, entry.sources);
    }
  }
});
po.observe({ type: 'layout-shift', buffered: true });
Field Role
value Shift score contribution
hadRecentInput User interaction window — excluded from CLS if true
sources Nodes that moved (where supported)
startTime When the shift happened

CLS aggregates shift scores across the page life (session windowing rules apply in the modern metric definition — follow current web.dev details for exact session windows).

Score intuition

Shift impact ≈ distance moved × viewport area fraction. A full-width banner shoving content down scores worse than a tiny badge nudge.

Common causes (field)

  1. Images/iframes without dimensions
  2. Fonts swapping metrics (font-display: swap without fallback metrics)
  3. Late ads/embeds injected above existing content
  4. Dynamic content insertion without reserved space
  5. Animating top/height instead of transform
<img src="/hero.avif" width="1200" height="630" alt="" />
.ad-slot {
  min-height: 250px;
  aspect-ratio: 4 / 1;
}

hadRecentInput matters

Clicks that expand an accordion intentionally move layout; those shifts are excluded when tied to recent input. Don’t “fix” CLS by ignoring product UX — fix unexpected shifts only.

Measuring with web-vitals

import { onCLS } from 'web-vitals';

onCLS(console.log);

Attribution builds help identify the largest shift’s nodes in supporting browsers.

Debugging workflow

  1. Lab: Performance panel → Experience / Layout Shift regions (Chrome).
  2. Filmstrip: watch jumps after late loads.
  3. Field: RUM CLS + page type + device.
  4. Fix: reserve space, font strategy, stable skeletons.

Product tactics: CLS optimization tactics.

Interview out-loud

“CLS sums unexpected layout shifts from the Layout Instability API. Entries with hadRecentInput don’t count. I reserve space for media and embeds, stabilize fonts, and use onCLS attribution to find the nodes that moved.”

Session windows (intuition)

Modern CLS aggregates shifts in session windows so a single long page doesn’t unfairly sum forever. Still, a big shift late in the visit hurts. Focus on the largest shift attribution nodes; fixing the top one often recovers the metric. Don’t chase 0.001 noise on desktop while mobile PDP banners score 0.25.

Further depth

Teams often under-invest in this topic until an incident or CWV regression. Schedule a one-hour drill: reproduce the failure mode in DevTools, list the top three mitigations for your stack, and file tickets with owners. Revisit after the next major feature that touches networking, rendering, auth, or third parties — those are the moments regressions land. Keep primary documentation links in the runbook so on-call is not searching chat history at 2am.

Concrete artifacts to leave behind: a short architecture note, a CI assertion or header snapshot, and a dashboard panel (lab or field) that would have caught the last bug. Teaching the rest of the team the mental model matters as much as the one-line fix.

Further reading

Related guides