How lazy loading works
The classic implementation uses an Intersection Observer:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // swap the placeholder for the real URL
observer.unobserve(img); // no further observation needed
}
});
}, { rootMargin: '200px' }); // start loading 200px before it appears
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));
rootMargin: '200px' is the buffer: the image starts loading while it is still 200 pixels of scrolling away. That removes the visible delay during fast scrolling.
The native version is simply an HTML attribute:
<img src="product.jpg" loading="lazy" alt="Product description" width="300" height="300" />
Width and height are mandatory — without them the browser reserves no space and you get CLS (Cumulative Layout Shift).
What benefits from lazy loading in e-commerce
Catalogue pages (PLP). Hundreds of product cards with images — without lazy loading the browser fetches every one at page open. With it, only the visible ones. Data saved: 60-90% for a typical listing of 48 to 96 products.
Recommendation widgets. Blocks such as “Similar products” or “Bought together” lower down the product page or on the cart page are natural candidates. The widget requests recommendations from the API only once it enters the viewport.
Images in the blog. Articles with illustrations inside long text — everything below the first screen goes lazy.
How not to damage Core Web Vitals
| Element | Recommendation |
|---|---|
| Hero image, logo | loading="eager" (the default) |
| The first three or four product cards | loading="eager" |
| The rest of the cards in a listing | loading="lazy" |
| Recommendation widgets | Lazy loading via Intersection Observer |
| Analytics snippets | async or defer |
Important: set explicit
widthandheighton every lazy image. Without them the browser reserves no space, which makes content jump during scrolling (CLS above 0.1 is a Core Web Vitals failure).