loading="lazy" as of 20251. 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:
- Faster initial page load — fewer bytes downloaded on first request
- Lower bandwidth costs — users who don't scroll don't pay for images they never see
- Better Core Web Vitals — particularly LCP (Largest Contentful Paint) and FID
- Reduced server load — fewer simultaneous image requests on page arrival
- Better mobile experience — critical on slower connections and data-limited plans
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.
<!-- 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
| Value | Behavior | When 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.
<img src="photo.jpg" alt="Team photo" loading="lazy">
No dimensions declared — browser can't reserve space. Layout shifts when image loads.
<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
// 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: 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: 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
// 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.
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:
- Your theme should not override or remove the
loadingattribute via custom filters. - The featured image and any image in the first viewport section should use
loading="eager"orfetchpriority="high"to ensure fast LCP. - Use a caching plugin (WP Rocket, W3 Total Cache) that respects lazy loading and doesn't strip the attribute.
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:
| Metric | What It Measures | Lazy 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.
<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.
<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 Type | Lazy 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.
-
✓
Hero / LCP image is NOT lazy loaded. Confirm in Lighthouse which element is your LCP. Ensure it has
loading="eager"(or no loading attribute) and ideallyfetchpriority="high". -
✓
All lazy images have width and height attributes. This prevents CLS. Even rough dimensions help — the browser just needs the aspect ratio to reserve space.
-
✓
Every lazy image has meaningful alt text.
loading="lazy"doesn't change alt text requirements. Screen readers still read alt text when the image does load. See our alt text best practices guide for complete examples. -
✓
Images use modern formats (WebP/AVIF). Lazy loading reduces request count; modern formats reduce file size. Together they're the biggest image performance win available.
-
✓
srcset is used for responsive images. Combine lazy loading with srcset so mobile users get appropriately sized images, not a 2000px photo scaled down.
-
✓
Intersection Observer has a root margin buffer. Set
rootMargin: '0px 0px 300px 0px'so images start loading 300px before they enter view — prevents visible loading lag on fast scrollers. -
✓
Lighthouse score verified after implementation. Run a before/after Lighthouse audit in Chrome DevTools to confirm LCP improved, CLS didn't worsen, and no new errors appeared.
-
✗
Don't lazy load above-fold images in a carousel. The first slide must load eagerly. Apply
loading="lazy"only to slides 2 and beyond. -
✗
Don't rely on JS-only lazy loading without a noscript fallback. If a user has JavaScript disabled, JS-based lazy loading shows broken images. Provide
<noscript>fallbacks or use nativeloading="lazy".
8. Frequently Asked Questions
-
Lazy loading is a technique that delays loading images until they are about to enter the user's viewport. Instead of downloading all images when the page loads, the browser only fetches images the user is actually about to see — reducing initial load time, bandwidth usage, and improving Core Web Vitals scores.
-
No — when implemented correctly, lazy loading improves SEO by boosting Core Web Vitals scores. Google's crawler handles native
loading="lazy"correctly and can still index lazy-loaded images. The critical rule: never lazy load your LCP image (usually the hero). That will hurt LCP scores and your rankings. -
loading="lazy"is a native HTML attribute — zero JavaScript, supported by 96%+ of browsers. Use it as your default. Intersection Observer is a JavaScript API that gives you more control: custom loading thresholds, fade-in animations, lazy loading of CSS backgrounds, and logic based on user behavior or connection speed. Use Intersection Observer when you need features beyond basic deferred loading. -
Never lazy load above-the-fold images. The hero image, top navigation images, and any image visible on initial load should load eagerly. For hero images, add
fetchpriority="high"to tell the browser to prioritize them. Only images below the fold — product grids, article body images, footer images — should useloading="lazy". -
Results vary by page composition, but Google's own data showed that enabling lazy loading in Chrome reduced image bytes by an average of 27% across all page loads. On image-heavy pages (e-commerce, galleries, blogs), the improvement is often 40–70% reduction in initial page weight. Combine with image compression and modern formats (WebP) for maximum gains.
-
Yes — always. The
loadingattribute doesn't change accessibility requirements. Screen readers still read thealtattribute when the image loads. Search engines index lazy-loaded images and use alt text to understand them. Every image, lazy-loaded or not, needs descriptive alt text. See our complete alt text best practices guide for writing rules and 50+ examples. -
Yes — since WordPress 5.5 (released August 2020), WordPress automatically adds
loading="lazy"to all images inserted via the block editor or classic editor. If you're on WP 5.5 or later, your images are already being lazy loaded. Check that your theme doesn't override this with a custom filter, and ensure your hero/featured images are excluded from lazy loading for LCP performance.
Related Guides
More ways to make your images faster and better for search.