Image Lazy Loading — loading='lazy', IntersectionObserver, and LCP
Lazy loading defers off-screen images until they're about to enter the viewport. Learn how HTML loading='lazy' works, when to use eager loading for LCP images,...
Lazy loading prevents off-screen images from blocking page load. The native loading="lazy" attribute works in all modern browsers. Use it everywhere except for your LCP (Largest Contentful Paint) image.
Use the Image Compressor to reduce image file size before deploying.
Native lazy loading
<!-- Defer loading until image is near viewport: -->
<img src="photo.jpg" loading="lazy" alt="Description" width="800" height="600">
<!-- NEVER use lazy on above-the-fold images: -->
<img src="hero.jpg" loading="eager" fetchpriority="high" alt="Hero" width="1200" height="600">
<!-- Or simply omit loading= (defaults to eager) -->
Supported in Chrome 77+, Firefox 75+, Safari 15.4+, Edge 79+.
How it works: The browser loads images once they come within ~1200px of the viewport (adjustable by browser heuristic). Exact threshold varies by connection speed.
The LCP image mistake
The most common performance mistake: lazy loading the hero/LCP image:
<!-- BAD: lazy loading the LCP image delays it further -->
<img src="hero.jpg" loading="lazy" alt="Hero">
<!-- GOOD: eager loading + high priority -->
<img src="hero.jpg" loading="eager" fetchpriority="high" alt="Hero"
width="1200" height="600">
Always fetchpriority="high" for your LCP image. This tells the browser to prioritize this resource over others.
Preload LCP image in <head>
<head>
<!-- Preload the LCP image before parser reaches <body>: -->
<link rel="preload" as="image" href="/hero.jpg" fetchpriority="high">
<!-- For responsive LCP image: -->
<link rel="preload" as="image"
href="/hero-800.jpg"
imagesrcset="/hero-400.jpg 400w, /hero-800.jpg 800w, /hero-1200.jpg 1200w"
imagesizes="(max-width: 600px) 100vw, 50vw"
fetchpriority="high">
</head>
IntersectionObserver for custom lazy loading
Useful when you need more control than loading="lazy":
function lazyLoadImages(selector = 'img[data-src]') {
const images = document.querySelectorAll(selector);
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
if (img.dataset.srcset) {
img.srcset = img.dataset.srcset;
}
img.removeAttribute('data-src');
img.removeAttribute('data-srcset');
obs.unobserve(img); // Stop observing once loaded
}
});
}, {
rootMargin: '200px 0px', // Start loading 200px before entering viewport
threshold: 0,
});
images.forEach(img => observer.observe(img));
}
// HTML with data-src instead of src:
// <img data-src="photo.jpg" src="placeholder.jpg" loading="lazy">
lazyLoadImages();
Background images (CSS)
Native loading="lazy" doesn’t work for CSS background-image. Use IntersectionObserver:
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const el = entry.target;
el.style.backgroundImage = `url(${el.dataset.bg})`;
el.classList.add('loaded');
observer.unobserve(el);
}
});
}, { rootMargin: '100px' });
document.querySelectorAll('[data-bg]').forEach(el => observer.observe(el));
<div class="hero" data-bg="/hero.jpg" style="background: #eee;">
<!-- Background image loads lazily -->
</div>
Skeleton loading during lazy load
/* Placeholder while image loads: */
img[loading="lazy"] {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
img.loaded {
animation: none;
background: none;
}
Always set width and height
Without width and height, the browser doesn’t know how much space to reserve — causing Cumulative Layout Shift (CLS):
<!-- BAD: No dimensions → layout shift when image loads -->
<img src="photo.jpg" loading="lazy" alt="Photo">
<!-- GOOD: Dimensions prevent CLS -->
<img src="photo.jpg" loading="lazy" alt="Photo" width="800" height="600">
<!-- CSS ensures responsiveness: -->
<style>
img { max-width: 100%; height: auto; }
</style>
Related tools
- Image Compressor — compress images for faster loading
- WebP vs JPEG vs PNG — choose the right format
- Responsive Images — srcset and CLS prevention
Related posts
- AVIF Image Format — Better Compression Than WebP and JPEG — AVIF offers 50% smaller files than JPEG at equivalent quality. Learn browser sup…
- Compress JPEG Online — Reduce Image File Size Without Losing Quality — JPEG compression lets you reduce image file sizes by 40–80% with minimal visible…
- Image Compression Formats — JPEG, PNG, WebP, AVIF Compared — JPEG, PNG, WebP, and AVIF compress images differently. JPEG is lossy and best fo…
- Image Compression Guide — Reduce File Size Without Losing Quality — Image compression reduces file size by removing redundant data. Here's how lossy…
- WebP vs JPEG vs PNG — Which Format to Use and How to Convert — WebP offers smaller file sizes than JPEG and PNG with comparable quality. Learn …
Related tool
Compress JPEG, PNG, and WebP images in your browser. Adjustable quality, batch mode. Files never leave your device.
Written by Mian Ali Khalid. Part of the Frontend & Design pillar.