Skip to content
Preptima
hardScenarioDesignMidSeniorStaff

Users say the page jumps around while it loads, but your synthetic audit reports no layout shift at all. How do you find it and fix it?

Layout shift is a field problem: it depends on network speed, consent banners, ads and personalisation that a lab run never sees. Collect layout-shift entries from real sessions with their source nodes, then reserve space for whatever moved.

6 min readUpdated 2026-07-29Target archetype: Big Tech, Enterprise Captive, Product Startup
Practice answering out loud

What the interviewer is scoring

  • Whether the candidate turns to field data with attribution rather than re-running the synthetic audit
  • Does the answer explain specifically why a fast, uninstrumented lab run cannot reproduce this
  • That space is reserved by declaring intrinsic dimensions rather than by guessing a min-height
  • Whether a late web-font swap is recognised as a shift with a font-metrics fix
  • Does the candidate separate shifts that follow user input from unexpected ones when reading the data

Answer

Why the lab run is clean and the users are not

Start by explaining the discrepancy rather than doubting the reports, because the reasons are specific and they tell you where to look. A layout shift happens when an element that has already been painted moves to a different position without the user having asked for it, and whether that happens depends almost entirely on the order and timing in which things arrive. A synthetic audit runs on a fast connection from a data centre, so an image, a font and a hero payload all land within a few milliseconds of each other and there is no interval during which the page is laid out one way and then another.

Then there is everything the lab run does not have. It usually has no consent banner, because that depends on the visitor's region and on whether a choice has already been stored. It frequently has no advertising or third-party embeds, because those are stripped from test builds or blocked by the harness. It is not signed in, so personalised modules, saved-address blocks and notification bars are absent. It has one viewport and one device pixel ratio, and a shift that only appears when a heading wraps to two lines is a function of viewport width. And it does not scroll, so anything that renders as it comes into view is never exercised at all.

None of that means synthetic testing is useless. It means layout shift in particular is a field metric, and the first move is to look at field data rather than to tune the lab.

Getting attribution out of real sessions

The browser exposes each individual shift, and crucially it names the nodes that moved, which turns a score into a list of culprits.

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // Shifts that follow a user interaction are flagged and are not counted
    // as unexpected, so exclude them before you go hunting.
    if (entry.hadRecentInput) continue;
    for (const source of entry.sources) {
      report({ value: entry.value, node: describe(source.node),
               from: source.previousRect, to: source.currentRect });
    }
  }
}).observe({ type: 'layout-shift', buffered: true });

Send a compact description of the largest shift per session, along with the viewport size, the connection type if available and the route, and the pattern usually resolves within a day. What you are looking for is not the average but the segmentation: shifts concentrated on one route, on narrow viewports, on first-time visitors, or on sessions from a region where the consent banner appears. That segmentation is the whole diagnosis, because each of those has a different fix.

Two properties of the score are worth knowing while reading the data. It is accumulated across the whole life of the page rather than measured during load, so a shift caused by content injected after five minutes of reading counts. And it is grouped into windows of consecutive shifts rather than summed indiscriminately, which is why a single burst of movement during load is scored as one event rather than as twenty. In Chrome's developer tools you can also record a throttled load and read the shifts back with their culprits, which is the right way to confirm a fix once field data has told you where to look.

Reserve the space, do not guess at it

Almost every fix is the same idea: make the box the right size before the content arrives. The mechanism differs by content type.

For images and video, provide the intrinsic dimensions. The width and height attributes are not legacy decoration; browsers derive an aspect ratio from them and reserve a correctly proportioned box even in a responsive layout where the rendered width comes from CSS. Where the markup cannot carry them, aspect-ratio in CSS does the same job. An image with neither collapses to nothing and then pushes everything below it down when the bytes arrive, which is the single most common source of this complaint.

For content that renders on the client after a fetch, the skeleton has to match the final dimensions rather than merely existing. A three-line placeholder replaced by a five-line result shifts the page, and a spinner in a small box replaced by a full table shifts it violently. Where the final size genuinely varies, decide the box from the layout rather than from the content: fix a height and scroll inside it, or accept a defined maximum and clamp.

For lazily rendered sections, content-visibility: auto is a real performance win and a real shift generator, because the browser needs an estimate of the skipped content's size. Supplying contain-intrinsic-size gives it that estimate, and the shift when a section is finally rendered is proportional to how wrong the estimate was.

Two small ones with outsized effect: scrollbar-gutter: stable removes the reflow that happens when a page grows tall enough to acquire a scrollbar, and any hover or focus style that adds a border should adjust an existing transparent border or use outline, since adding thickness to a box on hover moves its neighbours.

Fonts move text without moving any boxes

A web font that arrives after first paint replaces text already rendered in a fallback face, and because the two faces have different metrics the replaced text occupies a different number of lines or a different width. Nothing about your layout is wrong; the content changed size.

There are three levers and they compose. Preload the font files that are used above the fold so they arrive closer to first paint. Choose a fallback face whose metrics are close to the web font, which is easier for a body face than for a display one. And declare metric overrides on the @font-face rule for the fallback, using size-adjust and the ascent, descent and line-gap override descriptors, so the fallback is rendered at metrics matching the real font and the swap changes glyph shapes without changing line boxes. The last of those is the one candidates rarely know, and it is the only one that reduces the shift to nothing rather than merely shrinking the window in which it can happen.

The one you cannot fix with CSS

Content injected above existing content is a structural problem, not a styling one. A consent banner, a promotional bar or an error strip prepended to the document pushes the entire page down after the user has started reading. Reserving space for it is dishonest, because for returning visitors who have already consented the space stays empty. The options that do not shift anything are to position it out of flow as a fixed overlay, or to have it present in the initial server-rendered markup for the visitors who will see it, decided at the edge from the same signal that decides whether to show it at all. Ad slots are the same shape of problem with a narrower fix: a slot whose creative can be one of several heights should reserve the height you actually sell most often and be positioned so that a mismatch shifts nothing above it.

Layout shift is decided by arrival order, so it lives in field data and not in your audit. Get the shifted nodes out of real sessions, then make every box the right size before its content lands, including the boxes that text occupies.

Likely follow-ups

  • A consent banner is injected at the top of the document. What are your options, and which one does not shift anything?
  • An ad slot can render at one of three heights. How do you reserve space for that honestly?
  • The shift only happens when the user scrolls into a section that renders lazily. Which declaration is missing?
  • How would you stop this regressing without blocking every pull request on a synthetic audit?

Related questions

Further reading

core-web-vitalslayout-shiftrumweb-fontsimages