PannKs
UsefulTools & notes

Library

OverviewShort links
Prompts5
How To4
  • Free a Port a Dead Dev Server Still Holds
  • Ship a Fix Users Can't See (Service Worker Edition)
  • Stop the Page Jumping While It Loads
  • Truncate Text Without Breaking the Layout
← All How To

Stop the Page Jumping While It Loads

Layout shift is almost always four causes. How to find which one you have in about a minute, and the CSS that fixes each.

tags
cssperformanceweb
updated
2026-08-15

Loading…

← NewerShip a Fix Users Can't See (Service Worker Edition)Older →Truncate Text Without Breaking the Layout
Buy Me A Coffee

v.4.0.0 | Created By Next.JS 16.3.1

Pann Kaansadich © 2026

Find the shift first

Don't guess. Paste this into the console and reload — it prints each shift with the element that moved:

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.hadRecentInput) continue; // user-triggered, doesn't count
    console.log(entry.value.toFixed(4), entry.sources.map((s) => s.node));
  }
}).observe({ type: "layout-shift", buffered: true });

entry.value is that shift's contribution to CLS; anything over 0.01 is worth chasing. The nodes logged are the ones that moved — which is usually not the element causing the problem, but its neighbour.

Cause 1: images without dimensions

The browser cannot reserve space for a box whose size it learns only when the bytes arrive.

<img src="/hero.jpg" width="1600" height="900" alt="" />

The attributes are enough — modern browsers derive aspect-ratio from them, and CSS width: 100% still wins for the rendered size. For a container you size yourself:

.thumb {
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

Cause 2: web fonts

The fallback font has different metrics, so text reflows when the real font lands.

@font-face {
  font-family: "Rubik";
  src: url("/rubik.woff2") format("woff2");
  font-display: swap;
  size-adjust: 96%;      /* match the fallback's x-height */
}

font-display: swap avoids invisible text; size-adjust (plus ascent-override/descent-override) is what actually removes the reflow, by making the fallback occupy the same space. Next.js applies this automatically for next/font, which is a good reason to use it.

Cause 3: content injected above the fold

Banners, consent bars and "you have 1 new message" strips that mount after hydration push everything down. Reserve the space before it arrives:

.banner-slot {
  min-height: 4.8rem; /* the height it will be */
}

If the height is genuinely unknown, take it out of flow (position: fixed) so it cannot move anything.

Cause 4: animating layout properties

Transitioning height, top, margin or width re-runs layout on every frame and drags siblings with it. Animate transform and opacity instead — they run on the compositor and move nothing else:

/* costs layout every frame */
.panel { transition: height 200ms; }

/* free */
.panel { transition: transform 200ms; transform: translateY(-100%); }

The one-line audit

To see what is moving without the observer, in DevTools open the Rendering panel and switch on Layout Shift Regions. Shifted areas flash blue as they happen — fastest way to catch a shift you cannot reproduce on demand.