Lazy loading images is one of the highest-ROI performance changes you can make on a content-heavy website. Done right it cuts initial page weight by 50–80% on image-rich pages. Done wrong — especially on the LCP element — it tanks your Core Web Vitals score in a way that is hard to diagnose.
What changed in 2026
- Native
loading="lazy" is baseline everywhere — all modern browsers and the entire iOS/Android installed base support it. There is no longer a reason to ship an IntersectionObserver polyfill for this.
fetchpriority is supported across all major browsers — use it on your hero image to get a measurable LCP improvement.
- Next.js
<Image> is the template for framework-level image optimization — automatic WebP/AVIF conversion, responsive srcset, and built-in lazy loading.
- INP replaced FID in Core Web Vitals — images affect INP indirectly through layout shift and main-thread blocking; AVIF cuts both.
The basic pattern (plain HTML)
<!-- Above the fold — do NOT lazy load, add high priority -->
<img
src="/hero.jpg"
alt="Hero image"
width="1200"
height="630"
fetchpriority="high"
/>
<!-- Below the fold — lazy load -->
<img
src="/product.jpg"
alt="Product photo"
width="400"
height="300"
loading="lazy"
/>
Two attributes carry all the weight:
loading="lazy" — defers the request until the image is near the viewport.
width and height — reserve space so layout does not shift when the image loads.
Responsive images with srcset
<img
src="/photo-800.jpg"
srcset="
/photo-400.jpg 400w,
/photo-800.jpg 800w,
/photo-1600.jpg 1600w
"
sizes="(max-width: 600px) 100vw, 50vw"
alt="Team photo"
width="800"
height="533"
loading="lazy"
/>
srcset + sizes lets the browser pick the right resolution for the device. Without it you are sending a 1600px image to a 375px phone.
Next.js Image component
import Image from "next/image";
// Hero image — no lazy loading, high priority
export function Hero() {
return (
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={630}
priority // equivalent to fetchpriority="high" + no lazy
/>
);
}
// Product card — lazy load
export function ProductCard({ src, name }: { src: string; name: string }) {
return (
<Image
src={src}
alt={name}
width={400}
height={300}
// lazy is the default; no extra prop needed
/>
);
}
Next.js <Image> automatically:
- Converts to WebP/AVIF
- Generates a responsive
srcset
- Adds
loading="lazy" by default (use priority to override)
- Prevents CLS with the correct aspect ratio
React without Next.js
interface LazyImgProps {
src: string;
alt: string;
width: number;
height: number;
}
export function LazyImg({ src, alt, width, height }: LazyImgProps) {
return (
<img
src={src}
alt={alt}
width={width}
height={height}
loading="lazy"
decoding="async"
/>
);
}
decoding="async" tells the browser it can decode the image off the main thread — a small but free win.
When NOT to lazy-load
| Image |
Should lazy-load? |
Reason |
| Hero / banner at top of page |
No |
It is usually the LCP element |
| First visible product image |
No |
Above the fold |
| Images in carousels (all slides) |
No (first), Yes (rest) |
First slide is visible |
| Thumbnails in a long grid |
Yes |
Below the fold |
| Blog post body images |
Yes |
Reader scrolls to them |
| Open Graph / og:image |
N/A |
Not rendered in browser |
How to find your LCP image
# In Chrome DevTools > Performance tab, look for "LCP" in the timeline.
# Or use Lighthouse:
npx lighthouse https://yoursite.com --only-categories=performance
The LCP image is reported in the "Largest Contentful Paint" section. Verify it does not have loading="lazy" and does have fetchpriority="high".
Common mistakes
Lazy-loading the LCP image. This is the most common image performance mistake in 2026. The browser needs to load the LCP image as fast as possible; loading="lazy" tells it to wait. Result: LCP score drops, Core Web Vitals fail.
Missing width and height. Without explicit dimensions the browser does not know the aspect ratio until the image loads, causing a layout shift (high CLS). Always set them.
Using a JavaScript lazy loader when you do not need to. Every IntersectionObserver-based library adds JS parse time and a dependency. Native loading="lazy" is zero-JS and supported everywhere.
Not serving modern formats. A JPEG is typically 2–3× larger than the equivalent AVIF. Use Next.js <Image> or a CDN image transform (Cloudflare Images, Imgix) to serve AVIF automatically.
What to skip
- Blur-up placeholders for every image — they add JS complexity; use them only for images above the fold where CLS matters.
loading="eager" on below-the-fold images — this is the default before 2020; it is now the wrong default.
- Third-party lazy-load scripts for new projects — native is better.
FAQ
Does loading="lazy" affect SEO?
No. Google's crawler renders JavaScript and loads lazy images before indexing. Lazy loading does not hide images from search engines.
How far from the viewport does the browser start loading a lazy image?
It varies by browser and connection speed — roughly 1 000–2 000 px below the viewport on fast connections, closer on slow ones. This is intentional to avoid visible loading.
Should I lazy-load background images set with CSS?
Native loading="lazy" only applies to <img> tags. For CSS background images you still need JavaScript (IntersectionObserver) or a content-visibility trick.
What is the difference between loading="lazy" and decoding="async"?
loading="lazy" delays the network request. decoding="async" allows the decode to happen off the main thread after the image has downloaded. Use both together for below-the-fold images.
Where to go next