How to Implement Lazy Loading on Your Website
Lazy loading defers off-screen images and content until users scroll near them. Learn how to implement it natively and where to be careful.
Lazy loading is the practice of deferring the loading of non-critical resources — primarily images — until they’re needed. Instead of loading every image on a page at once, lazy loading only fetches images when the user scrolls close to them.
Done right, lazy loading reduces initial page weight, improves LCP (by giving the browser more bandwidth for critical resources), and cuts unnecessary data usage for users who don’t scroll the full page.
Native Lazy Loading with loading="lazy"
Modern browsers support lazy loading natively — no JavaScript required:
<img src="photo.webp" loading="lazy" alt="..." width="400" height="300" />
That’s it. The browser handles when to start fetching the image based on the scroll position and the network speed. This attribute is supported in all modern browsers (Chrome, Firefox, Safari, Edge).
Also works on iframes:
<iframe src="https://www.youtube.com/embed/VIDEO_ID" loading="lazy"></iframe>
The Most Important Rule: Never Lazy Load Your LCP Image
This is the most common lazy loading mistake. The LCP image needs to load as fast as possible — lazy loading it delays it until after the page renders.
<!-- Never do this to your hero/LCP image -->
<img src="hero.webp" loading="lazy" alt="..." />
<!-- Correct: no lazy loading, high priority -->
<img src="hero.webp" fetchpriority="high" alt="..." />
If your LCP score is poor and you have loading="lazy" on a prominent image, remove it — this alone can improve LCP by 0.5–1.5 seconds.
Lazy Loading Background Images
CSS background images aren’t lazy loaded natively. Use the loading="lazy" trick with a placeholder:
<div class="bg-image" data-src="photo.webp">
<img src="placeholder.webp" loading="lazy" alt="..." style="opacity: 0; position: absolute;" />
</div>
Or use the IntersectionObserver API to swap in the background image:
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const el = entry.target;
el.style.backgroundImage = `url(${el.dataset.src})`;
observer.unobserve(el);
}
});
}, { rootMargin: '200px' });
document.querySelectorAll('[data-src]').forEach(el => observer.observe(el));
Lazy Loading JavaScript Components
For React applications, lazy load components that aren’t immediately visible:
import { lazy, Suspense } from 'react';
// This component's code only loads when it's needed
const CommentSection = lazy(() => import('./CommentSection'));
function BlogPost() {
return (
<article>
<h1>Post Title</h1>
<p>Content...</p>
<Suspense fallback={<div>Loading comments...</div>}>
<CommentSection />
</Suspense>
</article>
);
}
Setting the Threshold
Native loading="lazy" uses a browser-determined threshold — images are fetched before they enter the viewport, so users don’t see a blank area while scrolling. The exact threshold varies by browser and network speed.
If you’re using IntersectionObserver directly, the rootMargin option controls when loading starts:
new IntersectionObserver(callback, {
rootMargin: '200px' // Start loading when image is within 200px of viewport
});
A rootMargin of 200–400px is a reasonable balance between not loading too early and avoiding visible blank areas during fast scrolling.
What to Always Lazy Load
- Gallery images below the fold (see our image optimization guide for format and sizing tips)
- User-generated content (comments, reviews)
- Social media embeds
- Maps and interactive widgets
- Content in tabs or accordions
- Product page images below the fold on e-commerce sites
What to Never Lazy Load
- Your LCP image
- Images above the fold
- Critical UI elements (navigation icons, logos)
- Small icons that are part of the initial layout
Measuring the Impact
After implementing lazy loading, run a test on debugutils.dev to see the change in total page weight and LCP. The waterfall will show fewer initial image requests, and the performance score should improve.