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.
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-paintInterpretation
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 -cInterpretation
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 --viewInterpretation
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.