A Beginner's Guide to Lazy Loading Images and Videos
Lazy loading defers off-screen images and videos until users scroll to them. Learn how to implement it correctly with native and JavaScript approaches.
Why download 20 images when the user can only see 3? Lazy loading defers loading of off-screen resources until the user scrolls near them, dramatically reducing initial page weight and improving LCP.
What Is Lazy Loading?
By default, browsers download ALL images and videos on a page as soon as they're discovered in the HTML — even images far below the fold that the user may never scroll to.
Lazy loading changes this behavior: resources are only downloaded when they're about to enter the viewport.
Impact Example
A blog post with 15 images:
| Strategy | Initial Download | LCP Impact |
|---|---|---|
| No lazy loading | All 15 images (3MB) | 4.5s |
| Lazy loading | 3 above-fold images (600KB) | 1.8s |
That's a 60% reduction in initial page weight.
Native Lazy Loading (The Easy Way)
Modern browsers support lazy loading natively with a single HTML attribute:
<img src="/photo.avif" loading="lazy" alt="Photo description"
width="800" height="600">
That's it. The browser handles everything — observing the viewport, downloading when needed, and even pre-fetching slightly before the image scrolls into view.
Browser Support
Native loading="lazy" is supported by 96%+ of browsers (Chrome, Firefox, Edge, Safari 15.4+).
Important Rules
DO lazy-load:
- Below-fold images
- Blog post images
- Product images in grids (except the first row)
- Footer images
DON'T lazy-load:
- The hero/banner image (your LCP element)
- Above-the-fold images
- Logo images
<!-- Hero image: load immediately, high priority -->
<img src="/hero.avif" alt="Hero" fetchpriority="high"
width="1920" height="1080">
<!-- Below-fold images: lazy load -->
<img src="/feature-1.avif" loading="lazy" alt="Feature 1"
width="600" height="400">
<img src="/feature-2.avif" loading="lazy" alt="Feature 2"
width="600" height="400">
Lazy Loading Videos
For <video> Elements
Use preload="none" to prevent downloading the video file:
<video preload="none" poster="/video-poster.jpg" controls
width="1280" height="720">
<source src="/video.mp4" type="video/mp4">
</video>
The poster image shows immediately. The video only downloads when the user clicks play.
For YouTube/Vimeo Embeds
YouTube embeds are heavy (~800KB of JavaScript). Use a facade pattern:
<!-- Show a thumbnail, load the player on click -->
<div class="video-facade" onclick="loadVideo(this)"
data-video-id="dQw4w9WgXcQ">
<img src="/youtube-thumb.jpg" alt="Video title"
width="1280" height="720" loading="lazy">
<button aria-label="Play video">▶</button>
</div>
<script>
function loadVideo(container) {
const id = container.dataset.videoId;
container.innerHTML = `<iframe src="https://youtube.com/embed/${id}?autoplay=1"
allow="autoplay" allowfullscreen width="1280" height="720"></iframe>`;
}
</script>
This saves ~800KB per YouTube embed.
Lazy Loading with Intersection Observer
For more control, use the Intersection Observer API:
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 viewport
});
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});
When to Use Intersection Observer Over Native
- You need to trigger animations when elements enter the viewport
- You want to load content other than images (components, data)
- You need fine control over the loading threshold
- You want to track scroll depth for analytics
Framework-Specific Implementation
Next.js
import Image from 'next/image';
// Automatically lazy-loaded (unless priority is set)
<Image src="/photo.jpg" alt="Photo" width={800} height={600} />
// NOT lazy-loaded (above-fold LCP image)
<Image src="/hero.jpg" alt="Hero" width={1920} height={1080} priority />
React
// Native lazy loading
<img src="/photo.jpg" loading="lazy" alt="Photo" width={800} height={600} />
// Or with a library like react-lazyload
import LazyLoad from 'react-lazyload';
<LazyLoad height={400} offset={200}>
<img src="/photo.jpg" alt="Photo" />
</LazyLoad>
Common Mistakes
1. Lazy Loading the LCP Image
If your hero image has loading="lazy", the browser deprioritizes it, increasing LCP. Always use loading="eager" (default) for above-fold images.
2. No Dimensions on Lazy Images
Without width and height, lazy-loaded images cause layout shifts when they load:
<!-- Bad: causes CLS -->
<img src="/photo.jpg" loading="lazy" alt="Photo">
<!-- Good: reserves space -->
<img src="/photo.jpg" loading="lazy" alt="Photo" width="800" height="600">
3. Lazy Loading Everything
Native lazy loading has a threshold — it starts loading images well before they enter the viewport. But if you lazy-load everything including near-viewport images, users might see blank spaces during fast scrolling.
Measure the Impact
After implementing lazy loading, check:
- LCP improvement (should decrease)
- Total page weight on initial load (should decrease significantly)
- CLS (should remain the same if dimensions are set)
- User experience during scrolling (images should load before entering viewport)
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