Short answer: Lazy loading delays loading of offscreen images, videos, and iframes until the user scrolls near them. It reduces initial page weight, speeds up load times, and saves bandwidth. Modern browsers support native lazy loading via the loading=’lazy’ attribute on img and iframe elements.
Key takeaways
- Native lazy loading with loading=’lazy’ is the simplest implementation.
- Intersection Observer offers more control for custom lazy loading.
- Always include width and height attributes to prevent layout shifts.
- Lazy load images below the fold; keep above-the-fold images eager.
- Apply lazy loading to videos, iframes, and background images too.
- Measure performance gains using Lighthouse or WebPageTest.
What you will find here
- What Is Lazy Loading and Why Does It Matter?
- Native Lazy Loading: The Easiest Way to Start
- Intersection Observer: Custom Lazy Loading with JavaScript
- Lazy Loading Videos and Iframes
- Lazy Loading Background Images
- Comparison: Native vs. Intersection Observer
- Best Practices and Common Pitfalls
- Performance Gains: What to Expect
- Testing Your Lazy Loading Implementation
Imagine landing on a web page that feels instant. Content appears as you scroll, images load just in time, and you never stare at a blank white box. That’s the promise of lazy loading—a technique that defers loading non-visible resources until the user needs them. For frontend developers, implementing lazy loading is one of the most impactful ways to improve web performance without a massive overhaul.

What Is Lazy Loading and Why Does It Matter?
Lazy loading means postponing the loading of resources that are not immediately needed. Instead of downloading every image, video, or iframe when the page first loads, the browser only loads what’s visible above the fold. The rest are fetched as the user scrolls down.
Why does this matter? Because download size directly affects load time. A typical webpage today weighs over 2 MB, with images accounting for the largest share. By deferring offscreen images, you can reduce initial page weight by 30% or more. This leads to faster Time to Interactive, better Largest Contentful Paint (LCP), and lower data usage for mobile users.
User expectations are high: they expect pages to load in under two seconds. Lazy loading helps you meet that benchmark without compromising on visual richness.
Native Lazy Loading: The Easiest Way to Start
The simplest method is native lazy loading using the loading attribute. Add loading="lazy" to your <img> and <iframe> tags, and the browser handles the rest. It’s supported in all modern browsers since 2020.
<img src="large-photo.jpg" alt="A scenic mountain landscape" loading="lazy" width="800" height="600">
Always include width and height attributes. This prevents Cumulative Layout Shift (CLS) because the browser reserves the space before the image loads. Without explicit dimensions, lazy loaded images can cause layout jumps when they finally appear.
For iframes, the approach is identical:
<iframe src="https://example.com/map" loading="lazy" width="600" height="450"></iframe>
Native lazy loading works well for most use cases. However, it gives you limited control. If you need to add custom effects like fading in images or tracking load events, you’ll want JavaScript-based solutions.
Intersection Observer: Custom Lazy Loading with JavaScript
The Intersection Observer API lets you efficiently detect when an element enters the viewport. It’s more powerful than older scroll-event-based approaches because it doesn’t cause layout thrashing.
Basic Implementation
Here’s a minimal example that lazy loads images using Intersection Observer:
const images = document.querySelectorAll('img[data-src]');
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);
}
});
});
images.forEach(img => observer.observe(img));
Use data-src to store the actual image URL. When the image enters the viewport, swap it into the src attribute and stop observing it to free memory.
Adding a Blur-Up or Placeholder Effect
A common enhancement is to show a tiny, blurred placeholder while the full image loads. Use two images: a tiny compressed version (e.g., 20px wide) as a background or separate <img>, and lazy load the full-resolution one on top. The placeholder loads instantly, and the crisp image fades in later.
Here’s a practical implementation using CSS and Intersection Observer:
<div class="lazy-image-wrapper">
<img class="placeholder" src="tiny-blur.jpg" alt="">
<img class="lazy" data-src="full-image.jpg" alt="Description" width="800" height="600">
</div>
<style>
.lazy-image-wrapper { position: relative; overflow: hidden; }
.lazy-image-wrapper img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; }
.placeholder { filter: blur(10px); transform: scale(1.1); }
.lazy { opacity: 0; transition: opacity 0.3s; }
.lazy.loaded { opacity: 1; }
</style>
<script>
const lazyImages = document.querySelectorAll('.lazy');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.onload = () => img.classList.add('loaded');
observer.unobserve(img);
}
});
}, { rootMargin: '200px' });
lazyImages.forEach(img => observer.observe(img));
</script>
The placeholder image should be extremely small in file size (under 1KB). The effect creates a smooth visual transition and improves perceived performance.
When to Use Intersection Observer Over Native
Use the Intersection Observer when you need: custom loading animations, load event callbacks, lazy loading of background images, or support for older browser versions (though native is now widely supported).

Lazy Loading Videos and Iframes
Lazy loading isn’t just for images. Videos and third-party embeds (like YouTube or maps) can also be deferred.
Video Lazy Loading
For HTML video elements, set preload="none" and use a poster image. Then, use Intersection Observer to start loading the video when the user scrolls near it. Avoid autoplaying videos with sound; they hurt performance and user experience.
<video preload="none" poster="thumbnail.jpg" width="640" height="360">
<source src="video.mp4" type="video/mp4">
</video>
One common mistake with video lazy loading is not handling the source element correctly. If you dynamically set the src on the video element, ensure you also trigger loading by calling video.load() after assigning the source. Otherwise, the browser may not fetch the video.
Iframe Lazy Loading
Third-party widgets like social media feeds or maps often contain heavy JavaScript. Defer them with loading="lazy" on the iframe, or for older browsers, add the src attribute only when the element comes into view.
For YouTube embeds, a clean technique is to replace the iframe with a clickable thumbnail image. When the user clicks, you swap in the real iframe and start playback. This reduces initial page weight significantly and only loads the YouTube player on demand.
<div class="youtube-placeholder" data-embed="dQw4w9WgXcQ">
<img src="thumbnail.jpg" alt="Video title" width="560" height="315">
<button class="play-button">Play</button>
</div>
<script>
document.querySelectorAll('.youtube-placeholder').forEach(placeholder => {
placeholder.addEventListener('click', () => {
const iframe = document.createElement('iframe');
iframe.src = `https://www.youtube.com/embed/${placeholder.dataset.embed}?autoplay=1`;
iframe.width = 560;
iframe.height = 315;
iframe.allow = 'autoplay';
placeholder.innerHTML = '';
placeholder.appendChild(iframe);
});
});
</script>
This approach also avoids the performance hit of loading the YouTube player for every visitor, even those who never play the video.
Lazy Loading Background Images
CSS background images are not covered by the native loading attribute. To lazy load them, you need JavaScript. A common technique is to set a data-background attribute on elements and swap it in using Intersection Observer:
<div class="hero" data-background="url('hero.jpg')"></div>
<script>
const bgElements = document.querySelectorAll('[data-background]');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const el = entry.target;
el.style.backgroundImage = el.dataset.background;
el.removeAttribute('data-background');
observer.unobserve(el);
}
});
});
bgElements.forEach(el => observer.observe(el));
</script>
One thing to watch out for: background images can cause layout shifts if their container has no intrinsic dimensions. Always set explicit heights or aspect ratios on these containers to reserve space.
Comparison: Native vs. Intersection Observer
| Feature | Native Lazy Loading | Intersection Observer |
|---|---|---|
| Browser Support | Modern browsers only | Polyfillable, wider support |
| Ease of Implementation | Very easy – one attribute | Moderate – requires JS |
| Customization | None | Full control |
| Performance | Excellent, browser-optimized | Good, but slight overhead |
| Built-in Layout Shift Prevention | Needs explicit dimensions | Needs explicit dimensions |
| Background Images | Not supported | Works with JavaScript |
| Event Tracking | No load events | Can track when loaded |
For most projects, start with native lazy loading and only add Intersection Observer if you need advanced features.
Best Practices and Common Pitfalls
Always Specify Dimensions
Lazy loading without width and height leads to layout shifts. Modern CSS can help with responsive images: use aspect-ratio or max-width: 100%; height: auto; on images that have explicit width and height attributes. For containers, set aspect-ratio to match the content dimensions to reserve space.
Don’t Lazy Load Above-the-Fold Content
Critical images (hero banners, logos, primary content) should load eagerly. Add loading="eager" or simply omit the attribute. Lazy loading above-the-fold images can delay LCP and hurt perceived performance. A good rule of thumb: if an image is visible on initial load, load it normally.
Set Appropriate Thresholds
With Intersection Observer, set a reasonable rootMargin to start loading images before they enter the viewport (e.g., rootMargin: '200px'). This ensures images are ready when the user scrolls. Be careful not to set too large a margin—loading everything too early defeats the purpose of lazy loading.
Test with Real Users and Tools
Use Lighthouse, WebPageTest, or Chrome DevTools to verify that lazy loading is working correctly. Check that images are not deferred unnecessarily and that no visible content flickers or shows empty space. Pay attention to the LCP element: it should not be lazy loaded.
Handle JavaScript Disabled Users
If you rely on JavaScript for lazy loading, provide fallback content (like a <noscript> tag with the full image) so users without JavaScript still see the content. For background images, consider using a default background that works without JS.
Performance Gains: What to Expect
Lazy loading can reduce initial page weight significantly. In a typical blog with many images, you might see Time to Interactive drop by 20-30%. Bandwidth usage decreases, especially on mobile networks. However, lazy loading does not improve perceived performance if not implemented correctly—test both with and without it.
Monitor key metrics: LCP should remain under 2.5 seconds, CLS below 0.1, and First Contentful Paint under 1.8 seconds. Lazy loading primarily affects LCP and initial load time, not CLS (if you use dimensions) or FCP.
Testing Your Lazy Loading Implementation
Before shipping, test on a throttled network (e.g., Fast 3G in Chrome DevTools) and verify that images load as you scroll. Check the Network panel to confirm that offscreen images are not requested until they come into view. Also, ensure that the placeholder images appear immediately and that there are no blank gaps.
One common issue: if you use loading="lazy" on images that are above the fold, you might see a delay in LCP. Use Performance panel to identify these cases and remove lazy loading from critical images.
Another test: load the page with JavaScript disabled. If you used Intersection Observer, users should still see content via <noscript> fallbacks. Without fallbacks, you risk blank pages for a non-trivial portion of users.
Implementing lazy loading is a practical step you can take today. Start with native attributes for images and iframes, then progressively enhance with Intersection Observer for custom behaviors. Your users will notice faster load times, and your performance scores will thank you.
Frequently asked questions
Does lazy loading affect SEO?
Lazy loading itself does not hurt SEO if implemented correctly. Search engines like Google can discover lazy loaded images as long as they are in the HTML markup (e.g., using data-src) and the page is rendered properly. Avoid hiding content from crawlers by using proper fallbacks or server-side rendering.
Can I lazy load background images in CSS?
Yes, you can lazy load CSS background images using JavaScript. One approach is to initially set the background-image to none, then apply the actual image URL via Intersection Observer when the element enters the viewport. Native lazy loading does not apply to CSS background images.
Is native lazy loading supported in all browsers?
Native lazy loading is supported in all major modern browsers: Chrome, Firefox, Edge, and Safari since 2020. Internet Explorer and older versions of Safari do not support it. For those, you can use a polyfill or fall back to Intersection Observer.
What is the difference between lazy loading and deferred loading?
Lazy loading defers resources until they are needed (usually when the user scrolls near them), while deferred loading delays resource loading until after the page has loaded, often used for JavaScript scripts. Deferred scripts still load eventually, but lazy loading only triggers on demand.
Should I lazy load images inside a carousel or slider?
Yes, lazy loading images inside carousels is recommended if the carousel has many slides that the user may not see. Load only the first few slides eagerly, then lazy load the rest as the user navigates. This reduces initial payload and improves performance.

Leave a Reply