⚡ Performance Guide

Lazy Loading Images:
The Complete 2025 Guide

Lazy loading is one of the highest-impact performance techniques available — and it takes less than five minutes to implement. This guide covers native HTML, Intersection Observer, React, Next.js, and WordPress, with working code you can copy today.

By IMGVO Team
Updated June 2025
14 min read
~2,900 words
27%
Reduction in image bytes from native lazy loading (Google Chrome data)
96.4%
Browser support for loading="lazy" as of 2025
~2.5s
Average LCP improvement on image-heavy pages after lazy loading

1. What Is Lazy Loading (and Why Does It Matter)?

By default, browsers load every image on a page — even images that are hundreds of pixels below the fold, far outside what the user can see. On a page with 30 images, that means 30 network requests fire simultaneously the moment the page starts loading, even though the user might never scroll far enough to see most of them.

Lazy loading flips this logic. Instead of fetching all images upfront, the browser waits until an image is about to enter the viewport, then loads it just-in-time. Images above the fold load immediately as before; everything else waits until the user scrolls toward it.

The performance benefits are compounding:

🔗 Related Guide

Before lazy loading, make sure your images are properly compressed and sized. Serving a 4MB image lazily is still a 4MB image. See our guide on how to compress images without losing quality.


2. Method 1: Native HTML — loading="lazy"

The simplest and most recommended way to lazy load images is the native HTML loading attribute. No JavaScript. No libraries. Just add one attribute to your <img> tag.

HTML
<!-- Before: eager (browser default) -->
<img src="hero-image.jpg" alt="Product hero image" />

<!-- After: lazy loaded -->
<img
  src="product-thumbnail.jpg"
  alt="Blue leather wallet, front view"
  loading="lazy"
  width="400"
  height="300"
/>

That's it. The browser handles everything else automatically.

The Three Values of loading

ValueBehaviorWhen to Use
loading="lazy" Defers loading until image is near viewport All below-the-fold images
loading="eager" Loads immediately (browser default) Above-the-fold images, hero images
loading="auto" Browser decides (same as no attribute) Don't use — unpredictable

Why You Must Include width and height

When you lazy load an image, the browser doesn't know its dimensions until it loads. Without declared dimensions, the browser allocates no space for the image — causing a layout shift when it finally loads. This is called Cumulative Layout Shift (CLS), and it's a Core Web Vitals metric that hurts both user experience and SEO.

✗ Causes Layout Shift (Bad for CLS)

<img src="photo.jpg" alt="Team photo" loading="lazy">
No dimensions declared — browser can't reserve space. Layout shifts when image loads.

✓ No Layout Shift (Good for CLS)

<img src="photo.jpg" alt="Team photo" loading="lazy" width="800" height="533">
Browser reserves exact space. Page layout stays stable as image loads.

Browser Support in 2025

loading="lazy" is supported by all major browsers — Chrome, Firefox, Safari, Edge, and Opera — with global coverage of 96.4% as of mid-2025. For the remaining 3.6% (mostly older Safari and IE), images simply load eagerly as they did before. No fallback code needed.


3. Method 2: Intersection Observer API

The native loading="lazy" attribute is great for static images, but the Intersection Observer API gives you fine-grained control: custom thresholds, animation triggers when images enter view, conditional loading based on connection speed, and support for non-<img> elements like background images.

Basic Intersection Observer Setup

JavaScript
// Step 1: Mark images with data-src instead of src
//   <img data-src="photo.jpg" alt="..." class="lazy-img" />

// Step 2: Create the observer
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;

      // Swap data-src → src to trigger the load
      img.src = img.dataset.src;

      // Optional: swap srcset for responsive images
      if (img.dataset.srcset) {
        img.srcset = img.dataset.srcset;
      }

      img.classList.remove('lazy-img');
      observer.unobserve(img); // Stop watching once loaded
    }
  });
}, {
  rootMargin: '0px 0px 200px 0px', // Load 200px before entering viewport
  threshold: 0.01
});

// Step 3: Observe all lazy images
document.querySelectorAll('img.lazy-img').forEach(img => {
  observer.observe(img);
});

Adding a Fade-In Animation

A common refinement is to fade images in smoothly as they load, rather than having them pop in abruptly:

CSS + JavaScript
/* CSS: Start images invisible */
img.lazy-img {
  opacity: 0;
  transition: opacity 0.4s ease;
}

/* JS: Add loaded class after image loads */
img.src = img.dataset.src;
img.addEventListener('load', () => {
  img.classList.add('loaded');
});

/* CSS: Fade in on load */
img.loaded {
  opacity: 1;
}

Lazy Loading CSS Background Images

CSS background-image properties can't use the native loading attribute. Intersection Observer handles this case:

HTML + JavaScript
<!-- HTML: Store URL in data attribute -->
<div
  class="hero-banner lazy-bg"
  data-bg="https://IMGVO.com/images/hero-bg.webp"
></div>

/* JS: Apply background when in viewport */
const bgObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const el = entry.target;
      el.style.backgroundImage = `url('${el.dataset.bg}')`;
      el.classList.remove('lazy-bg');
      bgObserver.unobserve(el);
    }
  });
});

document.querySelectorAll('.lazy-bg').forEach(el => bgObserver.observe(el));

4. Method 3: React, Next.js & WordPress

React — Manual Implementation

React JSX
// Simple: native loading attribute works in React out of the box
function ProductImage({ src, alt, width, height }) {
  return (
    <img
      src={src}
      alt={alt}
      loading="lazy"
      width={width}
      height={height}
      decoding="async"
    />
  );
}

// Advanced: custom hook with Intersection Observer
import { useRef, useState, useEffect } from 'react';

function useLazyImage(src) {
  const [imageSrc, setImageSrc] = useState(null);
  const imgRef = useRef();

  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        setImageSrc(src);
        observer.disconnect();
      }
    });
    if (imgRef.current) observer.observe(imgRef.current);
    return () => observer.disconnect();
  }, [src]);

  return { imgRef, imageSrc };
}

Next.js — Use the Built-In Image Component

Next.js has lazy loading built into its Image component. It's the recommended approach for all Next.js projects — it handles lazy loading, responsive sizes, WebP conversion, and CLS prevention automatically.

Next.js
import Image from 'next/image';

// Lazy loaded by default (all below-fold images)
function ProductCard() {
  return (
    <Image
      src="/product-photo.jpg"
      alt="Blue ceramic coffee mug, 12oz"
      width={400}
      height={400}
      // loading="lazy" is default — no need to write it
    />
  );
}

// Hero / LCP image: disable lazy loading, add priority
function HeroBanner() {
  return (
    <Image
      src="/hero.jpg"
      alt="IMGVO image compression tool interface"
      width={1200}
      height={600}
      priority  // Eager loads + preloads in <head>
    />
  );
}

WordPress

WordPress added native lazy loading to all images (via loading="lazy") starting from version 5.5. If you're on WP 5.5+, it's already enabled — no plugin needed.

For WordPress sites, the main thing to check:


5. Lazy Loading & Core Web Vitals

Google's Core Web Vitals are the primary performance metrics that influence search rankings. Lazy loading directly impacts two of the three:

MetricWhat It MeasuresLazy Loading Impact
LCP
Largest Contentful Paint
Time until the largest above-fold element renders Improves — if hero image is NOT lazy loaded. Hurts if hero IS lazy loaded.
CLS
Cumulative Layout Shift
Visual stability — how much elements shift Improves when lazy images have declared width and height. Hurts without dimensions.
INP
Interaction to Next Paint
Responsiveness to user interaction Indirect — lighter initial load = main thread less busy = faster interactions.

The LCP Rule — Critical to Get Right

The most common lazy loading mistake is accidentally applying it to the LCP image — usually your hero banner or the first large image on the page. Lazy loading this element delays its render and directly tanks your LCP score.

✗ Don't Do This — Hurts LCP

<img src="hero.jpg" alt="Hero" loading="lazy">
If this is the largest above-fold element, lazy loading it makes Google think your page is slow to render.

✓ Do This — Helps LCP

<img src="hero.jpg" alt="Hero" loading="eager" fetchpriority="high">
Explicitly eager-load and prioritize your LCP image. fetchpriority="high" tells the browser to download this before other resources.

To find your LCP element, run a Lighthouse audit in Chrome DevTools and look for the "LCP element" in the Performance section. Whatever element it identifies is the one that must never be lazy loaded.


6. What NOT to Lazy Load

Lazy loading has clear limits. Applying it to the wrong images causes measurable performance regressions. Here's a full breakdown:

Image TypeLazy Load?Reason
Hero / banner image Never Usually the LCP element — lazy loading kills your LCP score
Above-the-fold images (first viewport) Never User sees these immediately — loading them lazily causes flicker
Logo Never In the header — always visible, should load instantly
Product thumbnail grid (below fold) Yes Classic lazy load candidate — save bytes until user scrolls
Blog post inline images Yes Deep in the article — user must scroll to reach them
Footer images Yes Far below fold — ideal candidate
Avatar images in comment sections Yes Often dozens of images — lazy loading saves significant bandwidth
Carousel / slider images Careful Lazy load only slides 2+; slide 1 must load immediately
Open Graph image N/A Not a page image — defined in meta tags, not rendered

7. Lazy Loading Implementation Checklist

Use this checklist before deploying lazy loading on any page. Every item protects against a real performance regression or accessibility issue.


8. Frequently Asked Questions

Compress Before You Lazy Load

A well-optimized 80KB image loads fast even on slow connections. Start with compression, then add lazy loading for maximum performance.

Compress Images Free →

Related Guides

More ways to make your images faster and better for search.