Defer Offscreen Images: Lazy Load What Users Can't See
Loading images users haven't scrolled to yet wastes bandwidth and slows your page. Learn how to lazy load offscreen images the right way.
Every image your page loads upfront competes for bandwidth with critical resources. If users can't see an image yet, don't load it yet.
Why Offscreen Images Waste Performance
- Compete with critical resources — the browser has limited connections per domain
- Increase total page weight — users download images they may never see
- Slow LCP — bandwidth used by offscreen images delays the hero image
- Waste mobile data — especially painful on metered connections
How Lighthouse Flags This
Lighthouse calculates which images are outside the viewport at initial load and estimates the bytes that could be saved by deferring them.
How to Fix
1. Native Browser Lazy Loading (Simplest)
Add loading="lazy" to any <img> below the fold:
<!-- ❌ BAD — loads immediately even though it's offscreen -->
<img src="/images/product-gallery-5.jpg" alt="Product angle 5">
<!-- ✅ GOOD — browser loads it when user scrolls near -->
<img src="/images/product-gallery-5.jpg" alt="Product angle 5"
loading="lazy" width="600" height="400">
Important: Always include width and height so the browser reserves space and avoids layout shift.
2. Don't Lazy Load Above-the-Fold Images
Your hero image and any visible-on-load images should load eagerly:
<!-- Hero image — load immediately -->
<img src="/images/hero.webp" alt="Hero"
width="1200" height="600"
loading="eager"
fetchpriority="high">
<!-- Product images below fold — lazy load -->
<img src="/images/product-1.webp" alt="Product 1"
width="400" height="300"
loading="lazy">
3. Next.js Image Component
Next.js lazy loads images by default. Use priority for above-the-fold:
import Image from "next/image";
// Above the fold — loads immediately
<Image src="/hero.jpg" alt="Hero" width={1200} height={600} priority />
// Below the fold — lazy loaded automatically
<Image src="/product.jpg" alt="Product" width={400} height={300} />
4. Intersection Observer (Custom Control)
For more control over when images load:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute("data-src");
observer.unobserve(img);
}
});
}, {
rootMargin: "200px" // Start loading 200px before visible
});
document.querySelectorAll("img[data-src]").forEach(img => {
observer.observe(img);
});
5. Lazy Load Background Images with CSS
CSS background images don't support loading="lazy", so use Intersection Observer:
.hero-bg {
background-color: #1a1a2e; /* Placeholder color */
}
.hero-bg.loaded {
background-image: url("/images/bg-pattern.webp");
}
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add("loaded");
observer.unobserve(entry.target);
}
});
});
document.querySelectorAll(".lazy-bg").forEach(el => observer.observe(el));
6. Lazy Load Iframes Too
Embedded videos and maps are heavy. Lazy load them:
<iframe src="https://www.youtube.com/embed/xyz"
loading="lazy"
width="560" height="315"
title="Demo video"></iframe>
Measuring the Impact
Before and after lazy loading, check:
- Total transfer size — should drop significantly
- Number of requests on initial load — should decrease
- LCP — should improve as hero image gets more bandwidth
# Quick check with Lighthouse CLI
npx lighthouse https://yoursite.com --only-audits=offscreen-images
Quick Wins Checklist
- Add
loading="lazy"to all below-the-fold<img>tags - Ensure above-the-fold images have
loading="eager"(or no attribute) - Add
widthandheightto all lazy-loaded images - Use
fetchpriority="high"on LCP/hero images - Lazy load iframes (YouTube, maps)
- Test with Lighthouse to verify savings
Ready to stop wasting ad spend?
Track your landing page performance, monitor Core Web Vitals, and calculate exactly how much slow pages cost you.
Start Free — No Credit Card