How to Find the Source of Every Layout Shift in Chrome DevTools

This guide is the lab companion to Reducing Cumulative Layout Shift (CLS), part of Core Web Vitals & Measurement. RUM tells you CLS is 0.17 at p75 on a template; it rarely tells you which of the forty elements that moved was the cause. DevTools can, if you know where to look and how to distinguish the element that moved from the element that made it move.

That distinction is the crux. A layout shift entry lists the nodes whose position changed — the victims. The cause is usually something that changed size or was inserted above them: an image that loaded without dimensions, a font that swapped, an ad that resized. Effective debugging means walking from the victims back to the cause, at the exact moment the shift occurred.

From CLS number to root cause Workflow from a field CLS value to the specific element and change that caused the shift, using DevTools tools in sequence. From CLS number to root cause Reproduce same viewport + network Layout Shifts track find the cluster Shift sources which nodes moved Walk upward what changed above Confirm fix and re-record

Rapid Diagnosis

  • Match the field conditions. Use the viewport and throttling from your RUM's failing segment. Many shifts only occur at specific widths or when a resource is slow.
  • Enable the Layout Shift Regions overlay (Rendering drawer → "Layout Shift Regions") and reload. Moved areas flash blue; it is the fastest way to see where shifts happen.
  • Record a Performance trace with "Screenshots" on. The Layout Shifts track shows each shift as a diamond, grouped into clusters (session windows) with their scores.
  • Click the largest cluster. The summary lists each shift's score and the moved nodes; hovering a node highlights it on the page.
  • Note the timestamp. Look at the Network and Main tracks at that exact moment: what finished loading or executing just before?

Root Cause Analysis: The Five Usual Culprits

1. Media without reserved dimensions. An <img>, <video> or <iframe> with no width/height attributes or CSS aspect-ratio renders at zero height, then expands when its dimensions are known. In the trace, the shift coincides with the resource's response arriving.

2. Late-inserted content. Banners, ads, embeds, "related items" rails or client-rendered components inserted above existing content. The shift coincides with a script task or a fetch completing.

3. Web font swaps. Text re-renders in the web font with different metrics, changing line heights and wrapping. The shift coincides with a font request completing, and the moved nodes are text blocks and everything below them.

4. Layout property animations. Animations or transitions on top, height, margin or width shift elements every frame. The trace shows a series of small shifts in quick succession.

5. Hydration or client re-render. A framework replaces server markup with differently sized client markup. The shift coincides with the hydration long task.

Matching the shift timestamp to its cause For each common layout shift cause, what to look for in the trace at the moment of the shift. Matching the shift timestamp to its cause Cause In the trace at shift time Moved nodes Unsized media image/iframe response just ended Everything below the media Late insertion script task or fetch just ended Siblings after the insertion Font swap font request just ended Text blocks and below Layout animation many tiny shifts per second Animated element and siblings Hydration re-render long hydration task Component and below

Step-by-Step Workflow

1. Log shift sources with their rectangles

The Performance panel shows moved nodes, but the console gives you precise before/after rectangles you can reason about.

javascript
new PerformanceObserver((list) => {
  for (const e of list.getEntries()) {
    if (e.hadRecentInput) continue;
    console.groupCollapsed(`shift ${e.value.toFixed(4)} at ${Math.round(e.startTime)}ms`);
    for (const s of e.sources) {
      console.log(s.node, 'from', s.previousRect.toJSON(), 'to', s.currentRect.toJSON());
    }
    console.groupEnd();
  }
}).observe({ type: 'layout-shift', buffered: true });
// trade-off: sources lists at most five nodes per shift, prioritised by
// impact. A shift that moves fifty nodes shows five — usually enough, but the
// true cause may not be among them; walk up the DOM from the first one.

Expected outcome: a list of shifts with nodes and exact pixel deltas, which tells you the size of whatever was inserted or resized.

2. Find what changed above the first moved node

The delta in previousRect.y → currentRect.y is the height of the change. Walk the DOM upwards and to the preceding siblings of the first moved node looking for an element of that height that appeared or grew. The Elements panel's DOM breakpoints ("Break on → subtree modifications") on the parent pause execution at the insertion, showing the call stack responsible.

javascript
// Pause when anything is inserted into the main content area.
const target = document.querySelector('main');
new MutationObserver((records) => {
  for (const r of records) for (const n of r.addedNodes) {
    if (n.nodeType === 1 && n.getBoundingClientRect().height > 0) { debugger; }
  }
}).observe(target, { childList: true, subtree: true });
// trade-off: this pauses on every visible insertion, including legitimate
// below-the-fold ones. Narrow the selector once you know roughly where the shift is.

Expected outcome: the exact script and line that inserted the content.

3. Correlate with network and main-thread activity

In the trace, zoom to the shift and read the tracks above it. A font or image request ending at that instant, or a task from a third-party script, is almost always the trigger. Hover the request to see its initiator.

4. Confirm with a targeted change

Apply a minimal fix — add dimensions, reserve space, set font-display: optional temporarily — and re-record. If the shift disappears, you have the cause. If a smaller shift remains, repeat; pages often have two or three independent sources.

Correlating a shift with the resource that caused it Trace excerpt showing a font response and an ad script ending just before two layout shifts, identifying each shift's trigger. Correlating a shift with the resource that caused it Font web font request Ad ad script + creative Shifts 0.04 0.11 0ms 500ms 1000ms 1500ms 2000ms 2500ms 3000ms Each shift starts the instant a request completes — the font swap at 1.45s and the ad insertion at 2.3s.

Verification

After fixing, run the page with the Layout Shift Regions overlay on at each relevant width and confirm no blue flashes outside of user-initiated changes. Record a final trace and check that the Layout Shifts track has no clusters above 0.01. Then confirm in RUM with the web-vitals attribution build: attribution.largestShiftTarget for the template should change from the old culprit to nothing in particular, and p75 CLS should drop below 0.1 over the following days.

Shifts the Lab Tends to Miss

Some shifts depend on conditions a single lab run does not reproduce. Know them so you can force them deliberately.

Slow third-party responses. An ad or embed that loads in 300ms in your lab may take 3s in the field, landing after the content below it has rendered. Use request blocking with a delay (or a proxy) to slow specific third-party origins.

Cached vs uncached fonts. On repeat visits fonts are cached and render immediately without a swap. Test with the cache disabled and enabled; the first visit is usually the problem.

User-specific content. Logged-in banners, personalised recommendations and A/B test variants insert different content for different users. Test the variants your RUM shows are most common among high-CLS sessions.

Scroll-dependent shifts. Lazy-loaded images without dimensions only shift when scrolled into view. Scripted traces that do not scroll miss them entirely; add a slow scripted scroll to the recording.

Viewport resizes on mobile. The mobile browser's address bar collapsing changes the viewport height, which can resize vh-based elements. Use svh/lvh/dvh units deliberately and test on a real device.

FAQ

Why does the moved element not look like it moved?

Shift sources report elements whose layout position changed between frames, even by a few pixels, and even if they are below the fold or partly off-screen. A footer that moved 4px counts if it was in the viewport. The score weights by how much of the viewport moved and how far, so small movements of large elements still add up.

Does a shift inside a scroll container count?

Yes, if the shifted elements are visible within the viewport. Shifts are computed relative to the viewport, so content moving inside a visible scrollable panel contributes just like content in the main document.

Can I see layout shifts in Lighthouse instead of a trace?

Lighthouse's "Avoid large layout shifts" audit lists the elements that contributed most to CLS during its load, with scores, which is a quick first pass. It only covers the load it observed — one viewport, one network profile, no scrolling and no interaction — so it misses most of the shifts described in the section on what the lab tends to miss. Use it to triage, then reproduce in the Performance panel with the conditions your field data points at.