Image Lazy Loading — Deferring Offscreen Images Until Needed
In this tutorial, you will learn about Image Lazy Loading. We cover key concepts, practical examples, and best practices to help you master this topic.
Image lazy loading defers offscreen images using native attributes or Intersection Observer, reducing page weight and improving load time.
What You'll Learn
By the end of this tutorial, you'll understand multiple image lazy loading strategies, how to handle responsive images, placeholder techniques, and how to avoid common pitfalls with lazy loaded images.
Why It Matters
Images are the #1 cause of bloated web pages. Proper lazy loading reduces initial page weight by 50-80%, improves LCP (Largest Contentful Paint), and saves bandwidth for users on limited data plans.
Real-World Use
An e-commerce category page shows 48 product thumbnails. The first 8 are visible above the fold and load eagerly. The remaining 40 lazy load as the user scrolls. Initial page weight drops from 4MB to 0.7MB.
Image Lazy Loading Strategies
graph TD
A[Image Lazy Loading] --> B[Native loading=lazy]
A --> C[Intersection Observer]
A --> D[Blur-up placeholder]
A --> E[LQIP - Low Quality
Image Placeholder]
B --> F[Browser handles
loading timing]
C --> G[JavaScript controls
when to load]
D --> H[Tiny blurred image
as placeholder]
E --> I[Small thumbnail
before full image]
G --> J[Responsive srcset]
G --> K[Lazy load + fade-in]
style B fill:#27ae60,color:#fff
style C fill:#4a90d9,color:#fff
style D fill:#e67e22,color:#fff
style E fill:#f39c12,color:#fff
Native Lazy Loading with Placeholder
<!-- Strategy 1: Native lazy loading with blur-up -->
<style>
.image-wrapper {
position: relative;
overflow: hidden;
background: #f0f0f0;
}
.image-wrapper img {
width: 100%;
height: auto;
transition: opacity 0.3s ease;
}
.image-wrapper img.lazy {
opacity: 0;
}
.image-wrapper img.loaded {
opacity: 1;
}
.image-wrapper .placeholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
filter: blur(20px);
transform: scale(1.1);
transition: opacity 0.3s ease;
}
.image-wrapper .placeholder.hidden {
opacity: 0;
}
</style>
<div class="image-wrapper" style="aspect-ratio: 16/9;">
<!-- Blurred placeholder -->
<img src="placeholder-20px.jpg"
class="placeholder"
alt=""
aria-hidden="true"
width="100%" height="100%">
<!-- Full image with native lazy loading -->
<img src="photo-large.jpg"
srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1200.jpg 1200w"
sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px"
alt="Gallery photo"
loading="lazy"
class="lazy"
width="1200" height="675"
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');">
</div>
Intersection Observer Image Loader
// Advanced image lazy loader with responsive support
class ImageLazyLoader {
constructor() {
this.observer = new IntersectionObserver(
(entries) => this.loadImages(entries),
{ rootMargin: '200px 0px', threshold: 0.01 }
);
this.init();
}
init() {
document.querySelectorAll('img[data-src]').forEach(img => {
this.observer.observe(img);
});
}
loadImages(entries) {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const img = entry.target;
this.loadImage(img);
this.observer.unobserve(img);
});
}
loadImage(img) {
const src = img.dataset.src;
const srcset = img.dataset.srcset;
if (!src && !srcset) return;
// Set responsive sources
if (srcset) {
img.srcset = srcset;
img.sizes = img.dataset.sizes || '100vw';
}
if (src) {
img.src = src;
}
// Handle load event
img.onload = () => {
img.classList.add('loaded');
img.dispatchEvent(new CustomEvent('lazyLoaded'));
};
img.onerror = () => {
img.classList.add('error');
img.src = img.dataset.fallback || '/images/placeholder-error.jpg';
console.warn(`Failed to load: ${src || srcset}`);
};
// Remove data attributes
delete img.dataset.src;
delete img.dataset.srcset;
}
refresh() {
document.querySelectorAll('img[data-src]:not([data-lazy-processed])')
.forEach(img => {
img.dataset.lazyProcessed = 'true';
this.observer.observe(img);
});
}
}
// Initialize
const imageLoader = new ImageLazyLoader();
Blur-up and LQIP Techniques
// Blur-up image placeholder generation (server-side)
// Using Sharp to generate placeholders
const sharp = require('sharp');
async function generatePlaceholder(imagePath) {
// Generate 20px wide blurry placeholder
const placeholder = await sharp(imagePath)
.resize(20)
.blur(5)
.jpeg({ quality: 30 })
.toBuffer();
// Base64 encode for inline use
const base64 = placeholder.toString('base64');
const dataUri = `data:image/jpeg;base64,${base64}`;
// Also generate the responsive sizes
const sizes = [400, 800, 1200];
const variants = {};
for (const size of sizes) {
const buffer = await sharp(imagePath)
.resize(size)
.jpeg({ quality: 80 })
.toBuffer();
variants[size] = buffer;
}
return {
placeholder: dataUri,
width: (await sharp(imagePath).metadata()).width,
height: (await sharp(imagePath).metadata()).height,
variants
};
}
// HTML output with inline blur-up
function renderLazyImage(placeholder, srcset, alt, width, height) {
return `
<div class="lazy-image-wrapper" style="aspect-ratio: ${width}/${height}">
<div class="placeholder"
style="background-image: url('${placeholder}');
background-size: cover;
filter: blur(10px);
transform: scale(1.1);">
</div>
<img src="${srcset.split(',')[0].split(' ')[0]}"
srcset="${srcset}"
sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px"
alt="${alt}"
loading="lazy"
width="${width}" height="${height}"
onload="this.classList.add('loaded')">
</div>`;
}
Responsive Lazy Images
<!-- Complete responsive lazy loading pattern -->
<picture>
<!-- AVIF format (best compression) -->
<source
type="image/avif"
srcset="
photo-400.avif 400w,
photo-800.avif 800w,
photo-1200.avif 1200w
"
sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px">
<!-- WebP format (good compression) -->
<source
type="image/webp"
srcset="
photo-400.webp 400w,
photo-800.webp 800w,
photo-1200.webp 1200w
"
sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px">
<!-- JPEG fallback -->
<img src="photo-400.jpg"
srcset="
photo-400.jpg 400w,
photo-800.jpg 800w,
photo-1200.jpg 1200w
"
sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px"
alt="Responsive lazy loaded image"
loading="lazy"
width="1200" height="675"
decoding="async">
</picture>
Common Mistakes
- Lazy loading images without dimensions. Missing width/height causes layout shift when images load. Always specify dimensions or use aspect-ratio CSS.
- Using the same placeholder for all images. Generic gray boxes look unprofessional. Generate blur-up placeholders from the actual image.
- Not providing fallback for broken lazy images. If a lazy image fails to load, show a placeholder rather than a broken image icon.
- Lazy loading too many images at once. If the user scrolls quickly, many images may trigger simultaneously. Consider batch loading with throttling.
- Forgetting to set decoding="async". This allows the browser to decode images off the main thread, improving scroll performance.
Practice Questions
- What is the difference between native loading=lazy and Intersection Observer for images?
- How do blur-up placeholders improve perceived performance?
- Why should you set width and height attributes on lazy images?
- How does the
element work with lazy loading? - What is the LQIP technique and when should you use it?
Challenge: Build an image gallery with three lazy loading strategies: native loading=lazy, Intersection Observer with blur-up placeholders, and a
FAQ
Mini Project
Build a responsive image gallery with 50 photos: implement native lazy loading with blur-up placeholders, generate responsive srcsets in 3 sizes and 2 formats (WebP, JPEG), add a custom Intersection Observer with batch loading limits, and create a performance dashboard showing bandwidth saved.
What's Next
Images are handled. Now learn about Iframe Lazy Loading for embedded content and widgets.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro