Image Lazy Loading Best Practices 2026: Native, Intersection Observer, and LCP Trade-offs
Complete guide to lazy loading images in 2026 — use native loading="lazy" for most content, avoid lazy-loading LCP images, implement Intersection Observer for custom thresholds, handle responsive srcset, and measure real-world performance impact with CrUX.

Lazy loading images in 2026 is simpler than ever — loading="lazy" on an <img> tag defers offscreen images until the user scrolls near them. It reduces initial page weight, improves perceived load time, and saves bandwidth on long pages where users rarely scroll to the bottom.
But lazy loading is also the #1 way well-meaning developers accidentally ruin LCP by delaying the hero image. This guide covers when to lazy-load, when not to, how native lazy loading works, when to reach for Intersection Observer, and how to measure real impact.
What this guide covers
| Section | What you'll learn |
|---|---|
| Why lazy load images | Bandwidth, LCP, and user experience tradeoffs |
| Native lazy loading | loading="lazy", browser support, and how it works |
| When to lazy-load | Below-the-fold content, galleries, infinite scroll |
| When NOT to lazy-load | LCP images, above-the-fold heroes, critical UI |
| Intersection Observer | JavaScript API for advanced control |
| Lazy loading + srcset | Combining lazy load with responsive images |
| SEO implications | Does lazy loading hide content from search engines? |
| Measuring impact | Lighthouse, CrUX, and real-world metrics |
| Common mistakes | What breaks LCP, CLS, and discoverability |
Why lazy load images
Images are typically 45–55% of page weight on modern websites. On a long blog post with ten inline photos, a product grid with 50 thumbnails, or an infinite-scroll gallery, loading all images upfront wastes bandwidth and delays First Contentful Paint (FCP).
Benefits of lazy loading
- Faster initial load — smaller payload before interactive
- Bandwidth savings — users who never scroll don't download bottom images
- Better perceived performance — above-the-fold content appears faster
- Mobile data respect — especially valuable on metered connections
Risks of lazy loading
- LCP regression — lazy-loading the hero image delays Largest Contentful Paint
- Layout shift (CLS) — missing width/height causes reflow when images load
- JavaScript dependency — Intersection Observer patterns require JS; native loading="lazy" does not
The winning strategy: lazy-load everything below the fold, never lazy-load your LCP image.
Native lazy loading: the default approach
Since 2020, all major browsers support native lazy loading via the loading attribute:
<img src="/images/product.webp" alt="Product photo" loading="lazy" width="800" height="600" />
How it works
When loading="lazy" is set:
- Browser parses the HTML and sees the
<img>. - Browser checks if the image is near the viewport (within a threshold, typically 1–2 screens down).
- If offscreen, the browser defers the download until the user scrolls near it.
- When the user approaches, the browser fetches the image and paints it.
No JavaScript required. The browser handles distance thresholds, scroll detection, and fetch timing.
Browser support
| Browser | Support | Notes |
|---|---|---|
| Chrome | ✅ Since 76 (2019) | Stable |
| Safari | ✅ Since 15.4 (2022) | Stable |
| Firefox | ✅ Since 75 (2020) | Stable |
| Edge | ✅ Chromium base | Stable |
Fallback behavior: Browsers that don't support loading="lazy" ignore the attribute and load the image immediately — a graceful degradation.
loading attribute values
loading="lazy"— defer until near viewportloading="eager"— fetch immediately (same as default)- Omit attribute — defaults to
eager
Use lazy on below-the-fold images. Use eager or omit on above-the-fold images.
When to lazy-load images
Lazy-load when:
- Image is below the initial viewport — user must scroll to see it
- Page contains many images — blog posts, galleries, product grids
- Image is decorative or supplementary — inline figures, author avatars, thumbnail grids
- Infinite scroll or pagination — new images load as user scrolls
Example: blog post with inline photos
<article>
<!-- Hero above fold — do NOT lazy load -->
<img
src="/images/hero.avif"
alt="Article hero"
width="1200"
height="630"
fetchpriority="high"
/>
<p>Article intro...</p>
<!-- Inline images below fold — lazy load -->
<img
src="/images/chart.webp"
alt="Compression comparison chart"
loading="lazy"
width="800"
height="500"
/>
<p>More text...</p>
<img
src="/images/screenshot.png"
alt="UI screenshot"
loading="lazy"
width="1000"
height="700"
/>
</article>
When NOT to lazy-load
Never lazy-load:
- LCP (Largest Contentful Paint) image — typically the hero above the fold
- Images in the initial viewport — anything visible without scrolling
- Critical UI elements — logos, navigation icons, CTA backgrounds
- Small images unlikely to hurt performance — 20 KB thumbnails above fold
Why lazy-loading the LCP image hurts
LCP is often a hero <img>. When you add loading="lazy":
- Browser parses HTML.
- Browser sees
loading="lazy"and decides the image is low-priority. - Image fetch is delayed until browser confirms it's needed.
- LCP waits for that delayed fetch.
- Your Lighthouse LCP score goes from 2.1s to 3.8s.
Google's crawler and PageSpeed Insights flag this as "LCP element lazy-loaded" — a clear anti-pattern.
How to fix LCP lazy-loading
Remove loading="lazy" from the LCP image:
<img
src="/images/hero.avif"
alt="Hero image"
width="1920"
height="1080"
fetchpriority="high"
/>
Or explicitly set loading="eager":
<img src="/images/hero.avif" alt="Hero" loading="eager" fetchpriority="high" width="1920" height="1080" />
Use fetchpriority="high" to tell the browser this image is critical. This hints that it should fetch early, even competing with CSS and fonts.
Intersection Observer for custom lazy loading
Native loading="lazy" is sufficient for 90% of use cases. Use Intersection Observer when you need:
- Custom distance thresholds — load 500px before viewport instead of browser default
- Animation on reveal — fade in or slide up when image enters viewport
- Analytics tracking — log when images become visible
- Complex conditions — only load if user is on WiFi, or skip lazy-loading on fast connections
Basic Intersection Observer lazy load
<img data-src="/images/photo.webp" alt="Photo" class="lazy" width="800" height="600" />
const lazyImages = document.querySelectorAll('img.lazy')
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
img.classList.remove('lazy')
observer.unobserve(img)
}
})
})
lazyImages.forEach((img) => imageObserver.observe(img))
Custom threshold
Load images 1000px before they enter viewport:
const imageObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
observer.unobserve(img)
}
})
},
{
rootMargin: '1000px', // Load 1000px before viewport
},
)
When Intersection Observer is overkill
If you just want "load when near viewport," use native loading="lazy". Intersection Observer adds JavaScript, maintenance burden, and edge cases (disabled JS, older browsers).
Default to native. Graduate to Intersection Observer only when native is insufficient.
Lazy loading responsive images: srcset and sizes
loading="lazy" works perfectly with srcset and sizes:
<img
src="/images/hero-800.webp"
srcset="
/images/hero-400.webp 400w,
/images/hero-800.webp 800w,
/images/hero-1200.webp 1200w,
/images/hero-1920.webp 1920w
"
sizes="(min-width: 1024px) 1200px, 100vw"
alt="Responsive hero"
loading="lazy"
width="1200"
height="800"
/>
How it works:
- Browser evaluates
sizesto determine rendered width. - Browser picks the appropriate
srcsetcandidate (e.g., 1200w). - If
loading="lazy"is set and image is offscreen, browser defers fetch. - When user scrolls near, browser fetches the chosen srcset variant.
No special handling needed. Combine lazy loading with responsive images freely.
For art direction (different crops at different breakpoints), use <picture>:
<picture>
<source media="(min-width: 768px)" srcset="/images/hero-wide.avif" type="image/avif" />
<source media="(min-width: 768px)" srcset="/images/hero-wide.webp" type="image/webp" />
<source srcset="/images/hero-mobile.avif" type="image/avif" />
<source srcset="/images/hero-mobile.webp" type="image/webp" />
<img
src="/images/hero-mobile.jpg"
alt="Hero"
loading="lazy"
width="800"
height="600"
/>
</picture>
loading="lazy" applies to the <img> fallback; <source> elements inherit the behavior.
Read responsive images guide for full srcset and sizes patterns.
SEO and Googlebot crawling
Does lazy loading hide images from Google?
No. Google's crawler executes JavaScript and waits for lazy-loaded content to render. Native loading="lazy" and Intersection Observer patterns are both crawlable.
Best practices for SEO + lazy loading
- Always set
srcordata-src— don't rely on JavaScript-only image injection - Provide
alttext — crawlers read alt regardless of lazy loading - Use semantic markup —
<img>,<picture>,<figure>elements - Don't lazy-load critical structured data images — e.g., product schema ImageObject should be eager
Google's guidance
From Google Search Central (2023–2026):
- Native
loading="lazy"is fine and recommended for below-the-fold images. - Googlebot waits for Intersection Observer patterns to trigger.
- Avoid lazy-loading your LCP image — it hurts Core Web Vitals (a ranking signal).
Lazy loading is SEO-safe when used correctly.
For comprehensive image SEO guidance, see image SEO guide 2026.
Measuring lazy loading impact
Lighthouse and PageSpeed Insights
Run Lighthouse before and after enabling lazy loading:
- LCP — should improve or stay flat (not regress)
- Total page weight — should decrease
- Number of requests — should decrease on long pages
Red flag: If LCP increases, you lazy-loaded the wrong image.
Chrome User Experience Report (CrUX)
Lighthouse simulates a single page load. CrUX shows real-world data from actual users.
After deploying lazy loading, check CrUX via:
- PageSpeed Insights → Origin Summary tab
- Google Search Console → Core Web Vitals report
Wait 28 days for CrUX to reflect changes (rolling window).
Real User Monitoring (RUM)
Instrument your site with RUM (e.g., Sentry, New Relic, or custom analytics):
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name.includes('hero.avif')) {
console.log('LCP candidate loaded:', entry.startTime)
// Send to analytics
}
}
})
observer.observe({ type: 'resource', buffered: true })
Track LCP on the hero before/after lazy-loading adjacent images.
Common lazy loading mistakes
1. Lazy-loading the LCP image
Problem: Hero image has loading="lazy".
Fix: Remove lazy attribute or set loading="eager". Add fetchpriority="high".
2. Missing width and height
Problem: Image loads, layout shifts (CLS increases).
Fix: Always set dimensions:
<img src="/images/photo.webp" alt="Photo" loading="lazy" width="800" height="600" />
Or use CSS aspect-ratio on a container with object-fit.
3. Lazy-loading everything (including above-fold)
Problem: First-screen images delay, FCP and LCP suffer.
Fix: Only lazy-load images below the initial viewport. First 1–2 images should be eager.
4. JavaScript-only lazy loading without fallback
Problem: <img> has no src, only data-src. JS fails, image never loads.
Fix: Use native loading="lazy" when possible. If using Intersection Observer, set a low-quality src as fallback.
5. Over-aggressive thresholds
Problem: Intersection Observer rootMargin is 0, images load only when entering viewport — visible delay.
Fix: Use browser default (native lazy) or rootMargin like 500px or 1000px.
6. Forgetting srcset in Intersection Observer patterns
Problem: Custom JS lazy-loads only data-src, ignores data-srcset.
Fix: Handle both:
if (entry.isIntersecting) {
img.src = img.dataset.src
if (img.dataset.srcset) img.srcset = img.dataset.srcset
observer.unobserve(img)
}
Real-world strategies
Strategy 1: Native lazy for everything below fold (recommended)
<!-- Hero (above fold) -->
<img src="/hero.avif" alt="Hero" width="1920" height="1080" fetchpriority="high" />
<!-- All other images (below fold) -->
<img src="/photo1.webp" alt="Photo 1" loading="lazy" width="800" height="600" />
<img src="/photo2.webp" alt="Photo 2" loading="lazy" width="800" height="600" />
Pros: Simple, no JavaScript, works everywhere.
Cons: No custom thresholds or animations.
Strategy 2: Intersection Observer with animation
<img data-src="/photo.webp" alt="Photo" class="lazy fade-in" width="800" height="600" />
.lazy {
opacity: 0;
transition: opacity 0.3s;
}
.lazy.loaded {
opacity: 1;
}
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
img.classList.add('loaded')
observer.unobserve(img)
}
})
})
document.querySelectorAll('img.lazy').forEach((img) => observer.observe(img))
Pros: Polished UX, custom thresholds.
Cons: Requires JavaScript, more code to maintain.
Strategy 3: Hybrid (native + priority hints)
<!-- LCP hero -->
<img src="/hero.avif" alt="Hero" fetchpriority="high" width="1920" height="1080" />
<!-- Second-fold image (important but not LCP) -->
<img src="/intro.webp" alt="Intro" loading="lazy" width="1200" height="800" />
<!-- All other below-fold images -->
<img src="/photo1.webp" alt="Photo 1" loading="lazy" width="800" height="600" />
Pros: Balance between simplicity and control.
Cons: Requires understanding of LCP candidates.
Lazy loading checklist
Before shipping lazy loading:
- Identify your LCP image (Lighthouse → Diagnostics)
- Ensure LCP image does not have
loading="lazy" - Add
fetchpriority="high"to LCP image - Add
loading="lazy"to all below-the-fold images - Set
widthandheighton every<img>(prevents CLS) - Test with Lighthouse mobile (throttled 4G)
- Verify LCP did not regress
- Deploy and monitor CrUX for 28 days
Getting started today
- Run Lighthouse on your homepage and identify the LCP element.
- If it's an image, confirm it does not have
loading="lazy". - Add
loading="lazy"to the next 5 images below the fold. - Re-run Lighthouse and compare LCP and page weight.
- Roll out to all pages if LCP improves or stays flat.
Related reading: How images affect LCP · Fix large image LCP issues · Optimize images for Core Web Vitals · Responsive images guide
Frequently asked questions
Related tools
Continue in this topic
Pillar guideImage SEO in 2026: Alt Text, Filenames, Formats, and a Local Prep Workflow
Image SEO in 2026 is not a single checkbox. It is a pipeline: name files so crawlers understand them, write alt text so humans and screen readers get equal acc…
Ready to compress images without uploading them?
Open Asset Melt Studio