If your pages feel sluggish, images are almost always the culprit. They account for the largest share of bytes on the average web page, and most of them sit below the fold where visitors may never scroll. The fix is simple: lazy load them. In this practical tutorial, we’ll show you exactly how to lazy load images using the native HTML attribute, add a JavaScript fallback for edge cases, and share real performance numbers from a sample page we tested.
What Is Lazy Loading (And Why It Matters in 2026)
Lazy loading is a performance strategy that defers the loading of non-critical resources, like off-screen images, until the user is about to see them. Instead of downloading every image when the page loads, the browser only fetches what is needed.
The benefits are direct and measurable:
- Faster initial page load (lower LCP)
- Less bandwidth used for users who don’t scroll the full page
- Better Core Web Vitals scores, which Google uses as a ranking signal
- Lower server costs from fewer image requests

Method 1: The Native loading="lazy" Attribute
This is the easiest and most reliable way to lazy load images today. Every modern browser (Chrome, Edge, Firefox, Safari, Opera) supports it natively, with no JavaScript required.
Basic Syntax
<img src="product-photo.jpg" alt="Branded promotional pen" loading="lazy" width="800" height="600">
That’s the whole implementation. The browser handles distance-from-viewport calculations, prioritization, and fetching for you.
The Three Possible Values
| Value | Behavior | When to Use |
|---|---|---|
| lazy | Defers loading until the image is near the viewport | Below-the-fold images |
| eager | Loads the image immediately (default) | Above-the-fold hero images, logos |
| auto | Browser decides (rarely useful) | Generally avoid |
Critical Best Practices
- Always set
widthandheightattributes to prevent layout shift (CLS). - Never lazy load above-the-fold images. Doing so will hurt your LCP score.
- Use
fetchpriority="high"on your hero image to load it even faster. - Combine with modern formats like WebP or AVIF for compounding gains.

Method 2: IntersectionObserver Fallback
While native lazy loading covers more than 95% of users, you may still need finer control, for example to lazy load background images, iframes in legacy browsers, or to trigger custom animations when an image enters view. That’s where IntersectionObserver shines.
Step 1: Markup
Use a placeholder in src and store the real image URL in data-src:
<img class="lazy"
src="placeholder.jpg"
data-src="real-image.jpg"
alt="Custom branded mug"
width="800" height="600">
Step 2: JavaScript
document.addEventListener("DOMContentLoaded", function() {
const lazyImages = document.querySelectorAll("img.lazy");
if ("IntersectionObserver" in window) {
const imageObserver = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove("lazy");
observer.unobserve(img);
}
});
}, {
rootMargin: "200px 0px"
});
lazyImages.forEach(function(img) {
imageObserver.observe(img);
});
} else {
// Ultra-old browser fallback: just load everything
lazyImages.forEach(function(img) {
img.src = img.dataset.src;
});
}
});
The rootMargin: "200px 0px" option preloads images 200 pixels before they enter the viewport, which feels instant to the user.
Method 3: The Hybrid Approach (Recommended)
Use both. Native first, JavaScript only when needed:
<img src="image.jpg"
data-src="image.jpg"
loading="lazy"
class="lazy"
alt="Product"
width="800" height="600">
Then in JS, detect support and only run the observer when the native attribute isn’t supported:
if ("loading" in HTMLImageElement.prototype) {
// Native lazy loading is supported, do nothing
} else {
// Load IntersectionObserver script as fallback
}

Real Performance Comparison: Before vs After
We ran tests on a sample product gallery page with 32 images totaling 4.8 MB, using Chrome DevTools throttling at “Fast 3G” on a Moto G Power profile.
| Metric | Without Lazy Loading | With Lazy Loading | Improvement |
|---|---|---|---|
| Initial Page Weight | 5.2 MB | 0.9 MB | -83% |
| Largest Contentful Paint (LCP) | 4.1 s | 1.7 s | -58% |
| Time to Interactive (TTI) | 5.8 s | 2.4 s | -59% |
| Total Image Requests on Load | 32 | 6 | -81% |
| Lighthouse Performance Score | 62 | 94 | +32 pts |
The biggest takeaway: a single HTML attribute pushed our Lighthouse score from “needs improvement” to “good” without any build tooling, plugins, or refactoring.
Lazy Loading CSS Background Images
The native attribute only works on <img> and <iframe>. For background images, use IntersectionObserver with a CSS class swap:
const bgObserver = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
entry.target.classList.add("bg-loaded");
bgObserver.unobserve(entry.target);
}
});
});
document.querySelectorAll(".lazy-bg").forEach(function(el) {
bgObserver.observe(el);
});
Then in CSS:
.lazy-bg.bg-loaded {
background-image: url("hero.jpg");
}

Common Mistakes to Avoid
- Lazy loading the LCP image tanks your performance score instead of helping it.
- Forgetting
widthandheightcauses cumulative layout shift, which Google penalizes. - Loading every image at once on scroll defeats the purpose. Always observe individually.
- Using a heavy JavaScript library when the native attribute already works fine.
- Not testing on real devices, only on desktop, where bandwidth issues are invisible.
FAQ
Does lazy loading hurt SEO?
No. Google’s crawler supports both native lazy loading and IntersectionObserver-based lazy loading. As long as you use proper alt attributes and don’t hide images from the rendered DOM, your images will still be indexed.
Should I lazy load every image on the page?
No. Never lazy load images visible in the initial viewport, especially the LCP image. Lazy load only what’s below the fold.
Does loading="lazy" work on all browsers?
Yes for all modern browsers in 2026. Chrome, Firefox, Safari, Edge, and Opera all support it. For very old browsers, the attribute is simply ignored and the image loads normally, so there’s no breakage.
Why isn’t my loading="lazy" working?
The most common reasons are: (1) the image is already in the viewport so the browser loads it immediately, (2) you forgot to set width and height, or (3) you’re testing on a very fast connection where the browser preloads aggressively.
Is IntersectionObserver still needed in 2026?
Rarely for images, since native support is universal. However, it’s still the right tool for background images, custom triggers, infinite scroll, and animation-on-scroll effects.
Can I lazy load videos and iframes too?
Yes. <iframe loading="lazy"> is widely supported. For videos, use the preload="none" attribute and a poster image.
Final Thoughts
Lazy loading images used to require complex libraries and clever workarounds. In 2026, you can get 80% of the benefit by adding nine characters to each image tag. Combine the native attribute with an IntersectionObserver fallback for special cases, and you’ll deliver a faster, lighter, and more SEO-friendly experience to every visitor. Start with your heaviest gallery or product page, run a Lighthouse test before and after, and watch the numbers improve.

