← Debugging Guides

阅读中文版本

LCP Image Regression — loading=lazy on the Above-the-Fold Hero

Diagnose LCP regressions caused by lazy-loading the hero image, confirm with field and lab data, and reserve layout space while loading the fold eagerly.

beginnerdebuggingperformancelcpimages

Symptoms

  • Lighthouse LCP jumps from 1.8s to 3.5s after adding loading="lazy" to all images template-wide
  • Field data (CrUX) shows p75 LCP worsening on pages whose hero image now lazy-loads
  • The hero image flashes in noticeably late while the rest of the fold renders instantly

Possible causes

  • A template change applied loading="lazy" indiscriminately, including the above-the-fold LCP element
  • Lazy images wait for the browser to reach them in its priority pipeline, delaying the largest paint
  • The image lacks width and height attributes, adding layout shift to the late load
  • fetchpriority is unset, so even the eager hero competes with less important resources

Diagnosis steps

Identify the LCP element

DevTools → Performance → record a load → LCP marker; or PerformanceObserver with largest-contentful-paint

Interpretation

The element type and its load timing tell you whether the LCP is the image itself and how late it arrives relative to navigation start.

Check the attribute spread

grep -Rn "loading=\"lazy\"" templates/ | head; curl -s https://app.example.com | grep -o "loading=\"[a-z]*\"" | sort | uniq -c

Interpretation

Counting lazy attributes in served HTML reveals whether the hero was swept up in the blanket change.

Compare lab and field

npx lighthouse https://app.example.com --only-categories=performance --view

Interpretation

Lab confirms mechanism and regression size; CrUX field data confirms real-user impact on p75.

Root cause

Lazy loading defers an image until the browser judges it near the viewport, which places it behind layout, scripts, and other resources in the loading pipeline. Applied to the above-the-fold LCP image, it delays the largest paint of the page — the exact metric lazy loading was meant to protect.

Fix

Lazy-load only below-the-fold images. Keep the hero eager with explicit dimensions and fetchpriority="high", preload it when it is a background or dynamically inserted, and reserve aspect-ratio space so late loads do not shift layout.

Risk note

Eagerly loading every image above a guessed fold line hurts slow connections. Lazy-load from the second screenful onward, measured on real viewports.

Verify the fix

Re-run Lighthouse and watch CrUX: LCP returns to the pre-change level, lazy images still defer, and CLS stays flat thanks to reserved dimensions.

Symptom walkthrough

The regression tracks a single attribute. The served HTML shows it directly:

<img src="hero.jpg" loading="lazy" alt="...">

And the Performance panel shows the consequence: the LCP marker fires only after scripts and lower-priority resources, because the lazy hint removed the image from the early load pipeline. Confirm the element with an observer if you want field-grade evidence:

new PerformanceObserver((list) => {
  const e = list.getEntries().at(-1);
  console.log('LCP element:', e.element, e.startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });

Fix in practice

Treat the fold and the tail differently:

<!-- hero: eager, high priority, dimensioned -->
<img src="hero.jpg" width="1600" height="600"
     fetchpriority="high" alt="...">

<!-- below the fold: lazy, still dimensioned -->
<img src="gallery-1.jpg" width="800" height="600"
     loading="lazy" alt="...">

For CSS-background or script-inserted heroes, preload instead:

<link rel="preload" as="image" href="hero.jpg"
      fetchpriority="high">

The dimensions on both classes matter for the second-order metric: a late image without width and height shifts layout as it lands, converting an LCP problem into a CLS problem on top.