If your website loads dozens of images on the first paint, your Core Web Vitals are probably suffering. The good news: learning how to lazy load images is one of the fastest, cheapest performance wins you can implement, and in 2026 it requires almost no JavaScript at all.
In this practical tutorial, we will show you exactly how to implement native lazy loading with the loading attribute, build a robust IntersectionObserver fallback, and measure real LCP improvements before and after.
What Is Lazy Loading and Why Does It Matter for Core Web Vitals?
Lazy loading is a strategy that defers the loading of non-critical resources (typically below-the-fold images and iframes) until the user is about to see them. Instead of downloading every image on page load, the browser only fetches what is currently visible in the viewport.
The direct impact on Core Web Vitals:
- LCP (Largest Contentful Paint): fewer competing image requests means your hero image loads faster.
- INP (Interaction to Next Paint): less main-thread work during initial load.
- CLS (Cumulative Layout Shift): stays low if you set explicit width and height attributes.
- Total bandwidth: users who never scroll never download those images.

Method 1: Native Lazy Loading with the loading Attribute
This is the simplest and recommended approach in 2026. All major browsers (Chrome, Edge, Firefox, Safari) support it natively.
The Basic Syntax
<img src="product-photo.jpg"
alt="CAD workstation rendering"
width="1200"
height="800"
loading="lazy">
That is literally all you need. Just add loading=”lazy” to any image that is not visible in the initial viewport.
The Three Possible Values
| Value | Behavior | When to Use |
|---|---|---|
| lazy | Defers loading until image is near the viewport | Below-the-fold images |
| eager | Loads immediately (default) | Hero / LCP image |
| auto | Browser decides | Rarely needed |
Critical Rule: Never Lazy Load Your LCP Image
This is the mistake we see most often. If you lazy load your hero image, you will hurt your LCP, not improve it. The first one or two images visible above the fold should always use loading="eager" and ideally fetchpriority="high".
<img src="hero.jpg"
alt="Main banner"
width="1920"
height="900"
loading="eager"
fetchpriority="high">
Method 2: JavaScript Fallback with IntersectionObserver
While native support is now universal, you may still need JavaScript control for:
- Background images on
divelements (theloadingattribute does not work on backgrounds). - Custom logic, such as preloading 500px before the viewport.
- Lazy loading entire components, not just images.
Step 1: Prepare Your HTML
Use a data-src attribute instead of src, and a tiny placeholder:
<img class="lazy"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E"
data-src="real-image.jpg"
alt="Description"
width="800"
height="600">
Step 2: Write the IntersectionObserver
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",
threshold: 0.01
});
lazyImages.forEach(function(img) {
imageObserver.observe(img);
});
} else {
// Fallback for very old browsers
lazyImages.forEach(function(img) {
img.src = img.dataset.src;
});
}
});
The rootMargin: "200px 0px" tells the browser to start loading 200 pixels before the image enters the viewport, providing a smoother experience.

Real Before/After Core Web Vitals Results
We tested a product gallery page on a mid-range Android device with simulated 4G connection. The page contained 24 high-resolution product images.
| Metric | Before (no lazy load) | After (native lazy + eager hero) | Improvement |
|---|---|---|---|
| LCP | 4.1 s | 1.8 s | -56% |
| FCP | 2.3 s | 1.2 s | -48% |
| Total bytes | 8.4 MB | 1.6 MB | -81% |
| Requests on load | 62 | 18 | -71% |
| Lighthouse score | 54 | 92 | +38 pts |
The biggest gain came from setting fetchpriority="high" on the LCP image while lazy loading everything else.
Best Practices Checklist
- Always set explicit width and height attributes to prevent layout shift.
- Never lazy load your LCP image. Use
loading="eager"andfetchpriority="high". - Use modern formats: WebP or AVIF for additional savings.
- Combine lazy loading with responsive images via
srcsetandsizes. - Test with Chrome DevTools Lighthouse and PageSpeed Insights after deployment.
- For background images, use IntersectionObserver and toggle a CSS class.

Lazy Loading Background Images
Since loading="lazy" only works on <img> and <iframe>, here is a quick pattern for background images:
<div class="lazy-bg" data-bg="banner.jpg"></div>
<script>
const bgObserver = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.backgroundImage = `url(${entry.target.dataset.bg})`;
obs.unobserve(entry.target);
}
});
});
document.querySelectorAll(".lazy-bg").forEach(el => bgObserver.observe(el));
</script>
Common Mistakes to Avoid
- Lazy loading everything: hurts perceived performance for above-the-fold content.
- Forgetting dimensions: causes Cumulative Layout Shift.
- Using heavy JavaScript libraries: in 2026, native lazy loading has full support, no library needed.
- Not testing on real devices: throttled tests reveal issues that fast Wi-Fi hides.
FAQ
Does loading=”lazy” work in all browsers in 2026?
Yes. All evergreen browsers including Chrome, Edge, Firefox, Safari, Opera, and Samsung Internet fully support it. You only need a JavaScript fallback for very specific edge cases or older WebView environments.
Should I use a lazy loading library?
In most cases, no. Native lazy loading is faster because it does not require downloading and parsing JavaScript before images can start loading. Only use a library if you need advanced features like progressive blurry placeholders.
Will lazy loading hurt my SEO?
No, as long as you implement it correctly. Googlebot supports both the native loading attribute and IntersectionObserver. Just make sure images have proper alt text and are reachable in the rendered DOM.
What is the difference between lazy loading and async loading?
Lazy loading defers the resource until it is needed (near viewport). Async loading downloads the resource in parallel without blocking, but starts immediately. They solve different problems.
Can I lazy load iframes too?
Absolutely. Add loading="lazy" to <iframe> tags, especially useful for embedded YouTube videos or maps.
Conclusion
Knowing how to lazy load images is no longer optional in 2026. With one HTML attribute, you can cut your LCP in half, reduce bandwidth by over 80%, and significantly improve your Lighthouse score. Combine native lazy loading with proper image dimensions, modern formats, and a smart eager strategy for your hero image, and you will deliver a fast experience on every device.
At GraphiteOne CAD, we apply these exact techniques across our high-resolution product galleries and technical documentation to ensure engineers get the visuals they need without waiting.
