Lazy Loading Pitfalls: When It Hurts LCP Instead of Helping
Lazy loading is one of the most misapplied performance optimizations on the web. When used correctly on below-the-fold images and iframes, it saves bandwidth and speeds up initial page load. When applied incorrectly to the LCP element or above-the-fold content, it actively degrades Largest Contentful Paint by delaying the download of your most important resource.
The HTTP Archive found that 18% of sites apply loading="lazy" to their LCP image, directly causing poor LCP scores. This happens because developers apply lazy loading as a blanket optimization -- adding it to every image without considering which images are above the fold.
This guide explains exactly when lazy loading helps and when it hurts, how to identify the LCP element, and the correct loading strategy for every resource type on your page.
Expected results
Following all steps in this guide typically produces these improvements:
Before
4.1s
LCP score (Poor) -- hero image uses loading=lazy, delaying download until viewport intersection
After
1.5s
LCP score (Good) -- LCP image eager-loaded with fetchpriority=high, only below-fold images lazy-loaded
Step-by-step fix
Identify your LCP element and never lazy-load it
Before optimizing loading behavior, you must know which element is your LCP. The LCP element is the largest visible content element when the page first loads -- typically a hero image, featured product photo, or large heading. This element must load as fast as possible, which means it should never be lazy-loaded.
// Add this temporarily to identify your LCP element
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP Element:', {
element: lastEntry.element,
tagName: lastEntry.element?.tagName,
src: lastEntry.element?.src || lastEntry.element?.currentSrc,
size: `${lastEntry.size}px`,
loadTime: `${lastEntry.startTime.toFixed(0)}ms`,
url: lastEntry.url,
});
// Highlight it visually
if (lastEntry.element) {
lastEntry.element.style.outline = '4px solid red';
lastEntry.element.style.outlineOffset = '2px';
}
}).observe({ type: 'largest-contentful-paint', buffered: true });
// Also check: does the LCP element have loading="lazy"?
document.querySelectorAll('img[loading="lazy"]').forEach(img => {
const rect = img.getBoundingClientRect();
if (rect.top < window.innerHeight) {
console.warn(
'Above-the-fold image has loading="lazy"!',
img.src,
`Position: ${rect.top}px from top`
);
}
});
Apply the correct loading strategy per resource position
The loading strategy should depend on where the resource appears in the viewport. Above-the-fold content should load eagerly with high priority. Below-the-fold content should be lazy-loaded. The tricky part is the "fold" varies by viewport size, so you need a strategy that works across devices.
<!-- ABOVE THE FOLD: LCP image -- eager load + high priority -->
<img
src="/images/hero.avif"
alt="Hero image"
width="1200"
height="600"
fetchpriority="high"
decoding="async"
/>
<!-- Note: NO loading attribute = eager (default) -->
<!-- fetchpriority="high" boosts download priority -->
<!-- ABOVE THE FOLD: Secondary images -- eager, normal priority -->
<img
src="/images/logo.svg"
alt="Company logo"
width="150"
height="40"
/>
<!-- Default loading and priority is correct here -->
<!-- BELOW THE FOLD: Content images -- lazy load -->
<img
src="/images/feature-1.avif"
alt="Feature description"
width="600"
height="400"
loading="lazy"
decoding="async"
/>
<!-- FAR BELOW THE FOLD: Footer images -- lazy load -->
<img
src="/images/partner-logo.png"
alt="Partner company"
width="120"
height="60"
loading="lazy"
/>
<!-- IFRAMES: Always lazy load (unless above fold) -->
<iframe
src="https://www.youtube.com/embed/xyz"
loading="lazy"
width="640"
height="360"
title="Video title"
></iframe>
Fix common lazy loading anti-patterns
Several common patterns accidentally apply lazy loading to LCP images. JavaScript-based lazy loading libraries, CSS background images in carousels, and framework defaults can all cause the LCP image to be lazy-loaded without the developer realizing it.
<!-- ANTI-PATTERN: Lazy loading library applied to ALL images -->
<!-- Many libraries replace src with data-src for ALL images -->
<img
data-src="/images/hero.jpg"
class="lazyload"
alt="Hero image"
/>
<!-- The browser cannot download this image until the JS library
runs and replaces data-src with src! This adds 200-500ms. -->
<!-- FIX: Exclude above-the-fold images from the lazy library -->
<img
src="/images/hero.jpg"
alt="Hero image"
width="1200"
height="600"
fetchpriority="high"
/>
<!-- Use native loading="lazy" for below-fold images instead
of a JavaScript library. Native lazy loading is faster
and works without JavaScript. -->
<img
src="/images/below-fold.jpg"
alt="Content image"
width="600"
height="400"
loading="lazy"
/>
/* ANTI-PATTERN: Hero image as CSS background */
/* The browser discovers this image LATE -- after parsing
HTML AND downloading + parsing CSS */
.hero {
background-image: url('/images/hero.jpg');
background-size: cover;
height: 60vh;
}
/* FIX: Use an <img> element for the LCP image */
/* The browser discovers <img> during HTML parsing,
much earlier than CSS background-image */
<!-- Use img element for early discovery -->
<section class="hero">
<img
src="/images/hero.avif"
alt="Hero background"
width="1920"
height="1080"
fetchpriority="high"
decoding="async"
style="width:100%;height:60vh;object-fit:cover;"
/>
<div class="hero__content">
<h1>Your Headline</h1>
</div>
</section>
Configure framework-level lazy loading correctly
Frameworks like Next.js, Nuxt, and Gatsby have their own image components with lazy loading defaults. Understanding and overriding these defaults for the LCP image is essential. Most frameworks lazy-load by default and require an explicit opt-out for above-the-fold images.
import Image from 'next/image';
// LCP image: add priority prop to disable lazy loading
// and enable preloading
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority // Disables lazy loading + adds preload
sizes="100vw"
/>
// Below-fold image: default lazy loading is correct
<Image
src="/feature.jpg"
alt="Feature image"
width={600}
height={400}
// loading="lazy" is the default -- no changes needed
/>
<!-- Nuxt Image: LCP image with eager loading -->
<NuxtImg
src="/hero.jpg"
alt="Hero image"
width="1200"
height="600"
loading="eager"
fetchpriority="high"
sizes="100vw md:80vw lg:1200px"
/>
<!-- Below-fold image: lazy loading (default) -->
<NuxtImg
src="/feature.jpg"
alt="Feature image"
width="600"
height="400"
loading="lazy"
/>
---
// src/pages/index.astro
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---
<!-- LCP image: explicitly eager with high priority -->
<Image
src={heroImage}
alt="Hero image"
width={1200}
height={600}
loading="eager"
fetchpriority="high"
/>
<!-- Below-fold: lazy loading -->
<Image
src={featureImage}
alt="Feature"
width={600}
height={400}
loading="lazy"
/>
Implement an automated LCP lazy loading check
Manual checking is error-prone, especially when layouts change or new pages are added. An automated check in your CI/CD pipeline catches lazy-loaded LCP images before they reach production. This can be done with Lighthouse CI, a custom Playwright script, or a Performance Observer check in your testing suite.
// tests/lcp-lazy-check.spec.js
import { test, expect } from '@playwright/test';
const PAGES_TO_CHECK = [
'/',
'/blog/',
'/products/',
'/about/',
];
for (const pagePath of PAGES_TO_CHECK) {
test(`LCP image on ${pagePath} is not lazy-loaded`, async ({ page }) => {
await page.goto(pagePath, { waitUntil: 'networkidle' });
// Get the LCP element info
const lcpInfo = await page.evaluate(() => {
return new Promise((resolve) => {
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lcp = entries[entries.length - 1];
const el = lcp.element;
resolve({
tagName: el?.tagName,
loading: el?.getAttribute('loading'),
fetchpriority: el?.getAttribute('fetchpriority'),
src: el?.src || el?.currentSrc,
hasDataSrc: el?.hasAttribute('data-src'),
});
}).observe({ type: 'largest-contentful-paint', buffered: true });
// Fallback timeout
setTimeout(() => resolve(null), 5000);
});
});
// Assert LCP image is not lazy-loaded
if (lcpInfo?.tagName === 'IMG') {
expect(lcpInfo.loading).not.toBe('lazy');
expect(lcpInfo.hasDataSrc).toBe(false);
// Optionally check for fetchpriority="high"
expect(lcpInfo.fetchpriority).toBe('high');
}
});
}
Quick checklist
- LCP element identified for both mobile and desktop viewports
-
LCP image does NOT have
loading="lazy"ordata-src -
LCP image has
fetchpriority="high" -
Hero images use
<img>elements, not CSSbackground-image -
Framework image component has
priorityorloading="eager"for LCP image - JavaScript lazy loading libraries excluded from above-the-fold images
- Automated CI check prevents lazy-loaded LCP images from shipping
Frequently asked questions
loading=lazy delays the image download until the browser calculates that the image is near the viewport (typically within 1250-2500px). For above-the-fold images, this adds 200-500ms to load time because the browser waits for layout calculation instead of downloading immediately during HTML parsing. For the LCP image specifically, this delay directly increases LCP by the same amount.
No -- lazy loading is valuable for below-the-fold images. The correct approach is selective: LCP image gets fetchpriority=high with no lazy loading, other above-the-fold images use default eager loading, and all below-the-fold images use loading=lazy. A typical page might have 1-3 eager images and 10-20 lazy images.
Chrome has experimented with heuristics to ignore lazy loading for images that would be in the initial viewport, but this behavior is not reliable across all browsers or versions. Do not depend on browser heuristics -- explicitly set the correct loading behavior for each image based on its position.
Yes, for most cases. Native loading=lazy has zero JavaScript overhead, works before JavaScript executes, and is supported in all modern browsers. JavaScript libraries (lazysizes, lozad) add 2-10KB of code and require DOM manipulation. The only advantage of JS libraries is custom threshold control and support for CSS background images. Use native lazy loading unless you have a specific requirement that native cannot handle.
The first image in the carousel (the one visible on initial load) should be eager-loaded with fetchpriority=high. Subsequent carousel images should be eager-loaded without fetchpriority (they load at normal priority while the first image gets priority). Do not lazy-load any carousel images that the user will see within the first few seconds, as this causes visible loading delays during slide transitions.
Related resources
Complete LCP Guide
Deep dive into LCP -- thresholds, measurement, and optimization.
FixImage Optimization for LCP
Complete image optimization guide including formats, responsive images, and CDN.
TutorialHow to Lazy Load Everything
Comprehensive lazy loading guide for all resource types.
TutorialMeasure Core Web Vitals
Learn to identify your LCP element with lab and field tools.