Santaji GadeSEO, Core Web Vitals2 weeks ago36 Views

A product page with 60 photos doesn't need to load all 60 upfront. Here's working code for three ways to lazy load images native attribute, IntersectionObserver, and CSS backgrounds plus the one mistake that hurts LCP.
Table of Contents
ToggleA product page with 60 photos does not need to download all 60 the moment someone lands on it. Visitors typically see three or four before scrolling. Native JavaScript can lazy load images with a single HTML attribute, or with full custom control through the IntersectionObserver API, and this guide gives you working code for both.
Lazy loading images with native JavaScript means deferring image downloads until they are actually close to entering the viewport, using either the browser's built-in loading="lazy" attribute or the IntersectionObserver API for cases the native attribute cannot handle, like CSS background images.
We covered the strategy and SEO implications in our lazy loading images guide. This article is the implementation half: the actual code, working today, in three different approaches.
of global browser traffic supports the native loading="lazy" attribute directly
HTML attribute is all that's needed for the simplest lazy load images implementation
times you should ever lazy load your page's LCP or hero image
This is the simplest way to lazy load images, requiring no JavaScript at all for the majority of visitors.
<img src="product-photo.jpg" width="600" height="400" loading="lazy" alt="Blue ceramic coffee mug on wooden table">
width and height are required to prevent layout shift once the image actually loads
Never add loading="lazy" to your LCP or hero image. Doing so delays the browser's largest paint by 200-500ms and is flagged directly by Lighthouse as a performance anti-pattern.
According to DebugBear's guide to lazy loading with IntersectionObserver, this native browser API detects viewport intersection without the performance cost of manual scroll event listeners.
const lazyImages = document.querySelectorAll('img[data-src]'); const imageObserver = new IntersectionObserver((entries, observer) => { entries.forEach((entry) => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; img.removeAttribute('data-src'); observer.unobserve(img); } }); }, { rootMargin: '200px 0px' }); lazyImages.forEach((img) => imageObserver.observe(img));
rootMargin loads images 200px before they reach the visible viewport, avoiding a visible pop-in
<img data-src="product-photo.jpg" src="placeholder-tiny.jpg" width="600" height="400" alt="Blue ceramic coffee mug on wooden table">
Matching HTML: the real image URL sits in data-src until the observer swaps it in
According to PageSpeed Matters' complete 2026 lazy loading guide, the native loading attribute does not apply to CSS background images at all, since they are not standard img elements.
document.querySelectorAll('.lazy-bg').forEach((el) => { const bgObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add('bg-loaded'); entry.unobserve?.(entry.target) ?? bgObserver.unobserve(entry.target); } }); }); bgObserver.observe(el); });
Adds a class that triggers the actual background-image URL, defined separately in CSS
/* CSS: background-image only applies once .bg-loaded is added */ .lazy-bg.bg-loaded { background-image: url('hero-banner.jpg'); }
The class-swap pattern for background images referenced above
For most sites, no. Tap through both scenarios to see why.
According to PageSpeed Checklist's guide to native lazy image loading, roughly 95% of traffic already supports loading="lazy" natively, making a JavaScript fallback unnecessary complexity for most general-audience sites.
A site with a meaningful share of older Safari traffic can detect support and apply IntersectionObserver only where native support is missing, avoiding unnecessary JavaScript execution everywhere else.
if ('loading' in HTMLImageElement.prototype) { // Native lazy loading is supported, do nothing extra } else if ('IntersectionObserver' in window) { // Fall back to the IntersectionObserver pattern from Method 2 }
Feature detection: only runs the JavaScript fallback where native support is actually missing
Select the type of image to get the right technique for it.
Not every image should be lazy loaded the same way, or at all
According to Fasal Engineering's step-by-step guide to IntersectionObserver, the API uses the browser's compositor thread for intersection calculations, avoiding the main-thread cost that manual scroll event listeners used to require before this became possible to lazy load images natively at all.
According to Dean Hume's guide to lazy loading images using Intersection Observer, a graceful fallback for browsers lacking IntersectionObserver support simply loads all images immediately, rather than leaving them permanently hidden.
According to VitalsFixer's complete native lazy loading guide, a typical product page loading 60 images upfront, when only 3 or 4 are ever seen without scrolling, wastes significant bandwidth that lazy loading images correctly eliminates entirely.
| Rule | Why It Matters |
|---|---|
| Always set width and height | Reserves space before the image loads, preventing CLS |
| Never lazy load the LCP element | Adds 200-500ms delay to your largest paint metric |
| Use a low-quality placeholder for Method 2 | Avoids a jarring blank-to-image pop-in effect |
| Add fetchpriority="high" to hero images | Tells the browser to fetch it before other resources |
| Test with Lighthouse after implementation | Flags any accidental lazy loading of above-fold content |
According to web.dev's official guide to browser-level image lazy loading, running Lighthouse after implementation confirms two things at once: that below-fold images are correctly deferred, and that no above-fold or LCP image was accidentally caught by the same loading="lazy" attribute.
According to Divotion's guide to lazy loading images with IntersectionObserver, testing on an actual mobile connection, not just desktop Chrome DevTools throttling, catches real-world lazy loading images behavior that a fast local connection can mask entirely.
Native loading="lazy" covers roughly 95% of traffic with one HTML attribute
IntersectionObserver handles CSS backgrounds and custom control needs
Never lazy load the LCP or hero image; it delays your biggest paint metric
width and height attributes prevent layout shift once images load
A feature-detection fallback is optional, useful mainly for older Safari
Lazy-loaded images still need full, descriptive alt text










