Modern HTML5 in Production · Loading Performance

Images, LCP and Layout Shift

Why a marketing page felt slow and jumpy after an 'optimisation' that lazy-loaded every image, and how dimensions and loading priorities fix LCP and CLS.

Scenario

Example app: to speed up a landing page, a developer adds loading="lazy" to every image, including the large hero image at the top. Images have no width or height attributes because "CSS sizes them".

What you see in production

  • Largest Contentful Paint (LCP), the time until the main content shows, gets worse, not better.
  • The text jumps down when images arrive; users tap the wrong button (Cumulative Layout Shift, CLS, is poor).
  • Search Console flags the page's Core Web Vitals as "needs improvement".

The naive code

<img src="hero.jpg" alt="Hikers at sunrise" loading="lazy" class="hero">
<p>Book your trip today...</p>
<img src="gallery-1.jpg" alt="..." loading="lazy">

Reproduce it

The model estimates when the hero image finishes loading, and how far content shifts, for three versions of the page.

Hero image timing and layout shiftTry it yourself
const page = { htmlMs: 300, cssMs: 250, layoutMs: 100, heroDownloadMs: 600 };

function heroLcp({ lazy, priority }) {
  // the browser discovers the image while parsing HTML (preload scanner); lazy images wait for layout first
  const start = lazy ? page.htmlMs + page.cssMs + page.layoutMs : page.htmlMs + (priority ? 0 : 100);
  return start + page.heroDownloadMs;
}

function layoutShift({ dimensions }) {
  // without width/height the browser reserves 0px, then pushes text down by the image height when it arrives
  return dimensions ? 0 : 480;
}

const versions = {
  "lazy hero, no dimensions": { lazy: true, priority: false, dimensions: false },
  "eager hero, no dimensions": { lazy: false, priority: false, dimensions: false },
  "eager + fetchpriority=high + width/height": { lazy: false, priority: true, dimensions: true },
};
for (const [label, v] of Object.entries(versions)) {
  console.log(`${label.padEnd(42)} hero visible ~${heroLcp(v)} ms, content pushed down ${layoutShift(v)} px`);
}

Output

lazy hero, no dimensions                   hero visible ~1250 ms, content pushed down 480 px
eager hero, no dimensions                  hero visible ~1000 ms, content pushed down 480 px
eager + fetchpriority=high + width/height  hero visible ~900 ms, content pushed down 0 px

Root cause

  • Lazy loading delays an image until the browser knows it's near the viewport, which requires CSS and layout first. For the hero image, which is always in view, that only adds delay to LCP.
  • Without width and height, the browser doesn't know the image's size before it downloads, so it reserves no space and shifts the content later (CLS).

The fix

  • Hero and other above-the-fold images: no loading="lazy"; add fetchpriority="high" to the single most important image.
  • Below-the-fold images and iframes: loading="lazy".
  • Always set width and height to the image's intrinsic size (or use CSS aspect-ratio) and let CSS scale it (max-width: 100%; height: auto).
  • Make sure the hero is in the HTML (not injected by JavaScript or a CSS background), so the browser discovers it early. More in optimising LCP.

Trade-offs

  • fetchpriority="high" on many images cancels its benefit; use it for one.
  • Placeholders (blurred previews, dominant colours) improve perceived speed but add complexity.

Detect it in production

  • Real-user monitoring of LCP and CLS (the web-vitals library, Search Console's Core Web Vitals report).
  • Lighthouse in CI flags lazy-loaded LCP images and images without dimensions.

Key points

  • Never lazy-load the LCP (hero) image; prioritise it instead.
  • Always give images width and height (or aspect-ratio) to prevent layout shift.
  • Lazy-load only what's below the fold.

Practice questions

Why can lazy-loading the hero image make a page slower?

Answer

Lazy images wait until the browser has CSS and layout to know they're near the viewport, so the most important image starts downloading later, which worsens Largest Contentful Paint. Load it eagerly, ideally with fetchpriority="high".

How do width and height attributes prevent layout shift?

Answer

They give the browser the image's aspect ratio before it downloads, so it reserves the right space; CSS can still scale the image. Without them, content jumps when the image arrives.

What are the Core Web Vitals?

Answer

Largest Contentful Paint (loading), Interaction to Next Paint (responsiveness) and Cumulative Layout Shift (visual stability), measured from real users.

Open in the app to mark lessons complete, take notes and track your progress.