View contents
Lazy Loading Best Practices: When and When NOT to Use It
Introduction
Lazy loading is one of the most effective techniques for improving page performance—when used correctly. By deferring the loading of off-screen images, you reduce initial page weight, decrease Time to Interactive (TTI), and save bandwidth for users who don’t scroll to see all content.
However, misapplied lazy loading can hurt performance instead of helping it. The most critical mistake is lazy loading your LCP (Largest Contentful Paint) image, which can add 500ms+ to your most important Core Web Vital.
This comprehensive guide covers everything you need to know about implementing lazy loading effectively: when to use it, when to avoid it, placeholder strategies, and advanced optimization techniques.
Understanding Lazy Loading
What is Lazy Loading?
Lazy loading defers the loading of resources until they’re needed. For images, this typically means:
- Images start as placeholders (blank, blur, or low-quality)
- As the user scrolls, images near the viewport trigger loading
- Once loaded, the full image replaces the placeholder
Benefits of Lazy Loading
- Faster initial page load: Fewer resources to download upfront
- Reduced bandwidth: Users only download images they actually see
- Lower server load: Fewer simultaneous requests
- Better TTI: Less JavaScript blocking from image decoding
- Improved Core Web Vitals: When applied correctly
Native vs JavaScript Lazy Loading
There are two main approaches:
Native Lazy Loading (Recommended):
<img src="photo.jpg" alt="Description" loading="lazy">
JavaScript Lazy Loading:
// Using Intersection Observer
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
Native lazy loading is now supported by all major browsers (97%+ support) and should be your default choice.
When to Use Lazy Loading
Ideal Candidates for Lazy Loading
1. Images Below the Fold Any image that requires scrolling to see is a good candidate:
<!-- First screen: NO lazy loading -->
<img src="hero.jpg" alt="Hero">
<!-- Below fold: YES lazy loading -->
<img src="product-2.jpg" alt="Product" loading="lazy">
<img src="product-3.jpg" alt="Product" loading="lazy">
2. Product Grids (After First Row)
<!-- First row visible on load -->
<img src="product-1.jpg" alt="Product 1">
<img src="product-2.jpg" alt="Product 2">
<img src="product-3.jpg" alt="Product 3">
<img src="product-4.jpg" alt="Product 4">
<!-- Remaining products: lazy load -->
<img src="product-5.jpg" alt="Product 5" loading="lazy">
<img src="product-6.jpg" alt="Product 6" loading="lazy">
<!-- ... more products -->
3. Image Galleries and Carousels
<!-- Active slide: NO lazy loading -->
<img src="slide-1.jpg" alt="Slide 1" class="active">
<!-- Other slides: YES lazy loading -->
<img src="slide-2.jpg" alt="Slide 2" loading="lazy">
<img src="slide-3.jpg" alt="Slide 3" loading="lazy">
4. Blog Post Thumbnails
<article>
<img src="thumb.jpg" alt="Article" loading="lazy">
<h2>Article Title</h2>
</article>
5. User-Generated Content Comments, reviews, and social feeds often contain many images:
<div class="comment">
<img src="avatar.jpg" alt="User" loading="lazy">
<p>Comment text...</p>
</div>
When NOT to Use Lazy Loading
Critical Cases to Avoid
1. LCP Image (Most Important)
The Largest Contentful Paint image should load immediately:
<!-- WRONG: Don't lazy load LCP -->
<img src="hero.jpg" alt="Hero" loading="lazy">
<!-- CORRECT: No lazy loading, add priority -->
<img src="hero.jpg" alt="Hero" fetchpriority="high">
2. Above-the-Fold Content
Anything visible on initial page load:
<!-- All visible without scrolling: no lazy loading -->
<header>
<img src="logo.png" alt="Company Logo">
</header>
<main>
<img src="hero.jpg" alt="Hero" fetchpriority="high">
</main>
3. Small Images (< 10KB)
The overhead of lazy loading may exceed the savings:
<!-- Icons and small images: load normally -->
<img src="icon.svg" alt="" width="24" height="24">
4. Critical Product Images
On e-commerce product pages, the main product image is usually LCP:
<!-- Main product image: no lazy loading -->
<img src="product-main.jpg" alt="Nike Air Max" fetchpriority="high">
<!-- Thumbnail gallery: lazy load -->
<img src="product-angle-2.jpg" alt="Side view" loading="lazy">
<img src="product-angle-3.jpg" alt="Back view" loading="lazy">
Placeholder Strategies
Good placeholders prevent layout shift while communicating that content is loading.
1. Aspect Ratio Preservation
Always include width and height to prevent CLS:
<img
src="photo.jpg"
alt="Description"
width="800"
height="600"
loading="lazy"
>
With CSS:
img {
aspect-ratio: 4/3;
width: 100%;
height: auto;
}
2. Blur-Up Technique (LQIP)
Load a tiny, blurred version first:
<div class="image-container">
<img
src="photo-tiny.jpg"
data-src="photo.jpg"
alt="Description"
class="blur-up"
>
</div>
.blur-up {
filter: blur(20px);
transition: filter 0.3s;
}
.blur-up.loaded {
filter: blur(0);
}
3. Solid Color Placeholder
Use the dominant color from the image:
<div class="image-wrapper" style="background: #3498db;">
<img src="photo.jpg" alt="Description" loading="lazy">
</div>
4. Skeleton Loaders
Animated placeholder that indicates loading:
.skeleton {
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: skeleton 1.5s infinite;
}
@keyframes skeleton {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
Combining with Priority Hints
Use fetchpriority alongside lazy loading for optimal results:
<!-- High priority: LCP image -->
<img src="hero.jpg" alt="Hero" fetchpriority="high">
<!-- Default priority: above-fold secondary images -->
<img src="product.jpg" alt="Product">
<!-- Low priority with lazy load: below-fold images -->
<img src="gallery-5.jpg" alt="Gallery" loading="lazy" fetchpriority="low">
Priority + Preload for Critical Images
<head>
<!-- Preload LCP image -->
<link rel="preload" as="image" href="hero.webp" type="image/webp">
</head>
<body>
<img src="hero.webp" alt="Hero" fetchpriority="high">
</body>
Browser Loading Thresholds
Browsers have built-in thresholds for when lazy-loaded images start loading. Understanding these helps predict behavior.
Chrome’s Loading Distance
Chrome begins loading lazy images when they’re within a certain distance of the viewport:
| Connection Speed | Loading Distance |
|---|---|
| 4G | ~1250px |
| 3G | ~2500px |
| 2G | ~6000px |
This means on fast connections, images start loading sooner; on slow connections, the browser waits longer to save bandwidth.
Implications for Design
- Images in a carousel may start loading before the user swipes
- “Infinite scroll” content will preload ahead of the viewport
- Very long pages still benefit from lazy loading
Lazy Loading and SEO
Googlebot’s Behavior
Google can discover and index lazy-loaded images, but with caveats:
- Native lazy loading: Fully supported by Googlebot
- JavaScript lazy loading: Requires proper implementation
- Scroll-triggered loading: May not be indexed (Googlebot doesn’t scroll infinitely)
Best Practices for SEO
<!-- Include real src, not just data-src -->
<img
src="photo.jpg"
alt="Descriptive alt text"
loading="lazy"
>
<!-- BAD: No src until JavaScript runs -->
<img
data-src="photo.jpg"
alt="Photo"
src="placeholder.gif"
>
Structured Data Considerations
For product images and other schema markup, ensure the main image loads without JavaScript:
<script type="application/ld+json">
{
"@type": "Product",
"image": "https://example.com/product.jpg"
}
</script>
Framework-Specific Implementation
Next.js
import Image from 'next/image'
// LCP image: eager loading
<Image src="/hero.jpg" priority alt="Hero" />
// Below-fold: lazy loading (default)
<Image src="/gallery.jpg" alt="Gallery" />
Nuxt
<!-- LCP image -->
<NuxtImg src="/hero.jpg" preload alt="Hero" />
<!-- Lazy loaded (default) -->
<NuxtImg src="/gallery.jpg" alt="Gallery" loading="lazy" />
WordPress
Most modern WordPress themes apply lazy loading automatically. To exclude LCP images:
// In functions.php
add_filter('wp_img_tag_add_loading_attr', function($value, $image, $context) {
if (str_contains($image, 'hero-image')) {
return false; // Don't add loading="lazy"
}
return $value;
}, 10, 3);
Measuring Lazy Loading Impact
Key Metrics to Track
| Metric | What to Measure |
|---|---|
| LCP | Should improve or stay same (never increase) |
| TTI | Time to Interactive should decrease |
| Total Blocking Time | Should decrease |
| Page Weight | Initial download size should decrease |
| CLS | Should not increase (use aspect ratios) |
Tools for Measurement
- Lighthouse: Full performance audit
- Chrome DevTools Network Tab: See deferred requests
- WebPageTest: Filmstrip view shows loading sequence
- CrUX Dashboard: Real-user data over time
Common Mistakes and How to Fix Them
1. All Images Lazy Loaded
Problem: CMS or framework applies lazy loading globally Fix: Exclude first-screen images, especially LCP
2. Missing Dimensions
Problem: Images cause layout shift when loading Fix: Always include width/height or use aspect-ratio
3. Aggressive Thresholds
Problem: Images load too late, visible loading flicker Fix: Use native lazy loading or increase rootMargin
4. No Placeholders
Problem: Empty space jumps to filled space Fix: Implement blur-up, skeleton, or color placeholders
Implementation Checklist
- [ ] LCP image does NOT have
loading="lazy" - [ ] LCP image has
fetchpriority="high" - [ ] Above-fold images are not lazy loaded
- [ ] Below-fold images have
loading="lazy" - [ ] All lazy images have width/height attributes
- [ ] Placeholder strategy prevents layout shift
- [ ] SEO: Images have real src (not just data-src)
- [ ] Framework defaults reviewed and adjusted
- [ ] Performance tested with Lighthouse
Related Articles
- LCP Image Lazy Loading Explained - Why lazy loading LCP is wrong
- Image Fetchpriority Explained - Priority hints for images
- Image Preloading Guide - When to preload images
- CLS Optimization Guide - Preventing layout shift
References
- MDN - loading attribute - Technical specification
- web.dev - Browser-level lazy loading - Implementation guide
- Chrome Developers - Lazy loading images - Lighthouse documentation
- web.dev - Native lazy loading - Deep dive on native implementation