Performance & SEOMehdi Tareghi10 min read

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.

Diagram showing lazy-loaded images appearing as user scrolls viewport

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

SectionWhat you'll learn
Why lazy load imagesBandwidth, LCP, and user experience tradeoffs
Native lazy loadingloading="lazy", browser support, and how it works
When to lazy-loadBelow-the-fold content, galleries, infinite scroll
When NOT to lazy-loadLCP images, above-the-fold heroes, critical UI
Intersection ObserverJavaScript API for advanced control
Lazy loading + srcsetCombining lazy load with responsive images
SEO implicationsDoes lazy loading hide content from search engines?
Measuring impactLighthouse, CrUX, and real-world metrics
Common mistakesWhat 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:

  1. Browser parses the HTML and sees the <img>.
  2. Browser checks if the image is near the viewport (within a threshold, typically 1–2 screens down).
  3. If offscreen, the browser defers the download until the user scrolls near it.
  4. 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

BrowserSupportNotes
Chrome✅ Since 76 (2019)Stable
Safari✅ Since 15.4 (2022)Stable
Firefox✅ Since 75 (2020)Stable
Edge✅ Chromium baseStable

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 viewport
  • loading="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":

  1. Browser parses HTML.
  2. Browser sees loading="lazy" and decides the image is low-priority.
  3. Image fetch is delayed until browser confirms it's needed.
  4. LCP waits for that delayed fetch.
  5. 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:

  1. Browser evaluates sizes to determine rendered width.
  2. Browser picks the appropriate srcset candidate (e.g., 1200w).
  3. If loading="lazy" is set and image is offscreen, browser defers fetch.
  4. 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 src or data-src — don't rely on JavaScript-only image injection
  • Provide alt text — 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

<!-- 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 width and height on 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

  1. Run Lighthouse on your homepage and identify the LCP element.
  2. If it's an image, confirm it does not have loading="lazy".
  3. Add loading="lazy" to the next 5 images below the fold.
  4. Re-run Lighthouse and compare LCP and page weight.
  5. 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

Performance & SEO
View all guides
Pillar guide

Image 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