A blog homepage displayed 50 article thumbnails. All 50 loaded on page load, regardless of scroll position. Analytics showed 70% of visitors scrolled past only 5-6 articles before leaving. Those visitors downloaded 44 images they never saw. 3MB of wasted per visit. On mobile, initial page load took 4 seconds because the browser was busy fetching images below the fold.
Adding loading="lazy" to the below-fold images cut initial page weight from 3.5MB to 400KB. First Contentful Paint dropped from 4 seconds to 1.1 seconds. The images below the fold loaded silently as users scrolled, completely invisible to them.
Lazy loading is a simple trade: defer downloading resources that are not visible yet. The initial paint is faster because the browser focuses its bandwidth on what the user can actually see. Resources that are never scrolled to are never downloaded at all.
Native lazy loading (HTML attribute):
<img src="photo.jpg" loading="lazy" alt="Product photo" width="400" height="300">
One attribute. No JavaScript. The browser handles everything: it monitors the viewport, starts fetching the image when it is roughly 1250px from the visible area (the exact threshold varies by browser and connection speed), and renders it when the data arrives.
Supported on 96%+ of browsers as of 2024. The only notable gap is very old Safari versions (pre-15.4).
JavaScript IntersectionObserver (custom behavior):
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
}, { rootMargin: '200px' });
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));
IntersectionObserver gives you control over the trigger distance (rootMargin), lets you lazy-load non-image content (iframes, video, heavy components), and works with custom placeholder animations. But for simple image lazy loading, the native attribute is simpler and more reliable.
Lazy load aggressively:
| Content | Why |
|---|---|
| Below-fold images | Not visible on initial paint |
| Embedded YouTube/Vimeo iframes | Each iframe downloads 500KB-1MB of player code |
| Third-party widgets (maps, chat, social) | Heavy and often never interacted with |
| Comments section | Most readers do not scroll to comments |
| Image carousels (slides 2+) |
Lazy loading is one of the easiest performance wins to measure because the before/after is dramatic:
eBay (2019 case study): Added native lazy loading to search result images. Initial payload dropped by 100 images worth of data per page. First Input Delay improved by 12%.
Chromium team measurement: Sites using loading="lazy" on below-fold images saw median First Contentful Paint improve by 200-400ms on mobile connections.
The math: A page with 40 product images at 60KB each (WebP, properly sized) loads 2.4MB total. If only 8 images are above the fold, lazy loading reduces the initial download from 2.4MB to 480KB. The remaining 1.9MB loads incrementally as the user scrolls.
On a 3G connection (750KB/s effective), that is the difference between a 3.2-second and a 0.6-second initial image load.
Deferring work until it is needed is obviously good, and it has two specific ways of making a page worse that are easy to walk into.
Layout shift. An image that loads later arrives into a space the browser did not reserve, so everything below it jumps down. The user is reading a paragraph and it moves. This is Cumulative Layout Shift, it is a ranked Core Web Vital, and the fix is entirely mechanical: give every lazily loaded element an explicit width and height, or an aspect ratio, so the space exists before the content does.
<!-- The dimensions reserve the box. Without them, this image shifts the page. -->
<img src="hero.jpg" width="1200" height="630"
loading="lazy" decoding="async" alt="...">
Lazy loading the thing the user is already looking at. The most common mistake, and it makes the metric that matters worse rather than better. Applying loading="lazy" to an above-the-fold hero image means the browser deliberately delays the single most important asset on the page, and Largest Contentful Paint gets worse. Anything visible without scrolling must load eagerly, and the genuinely important one should be hinted with fetchpriority="high".
Then the subtler costs:
3 questions - Score 80% to pass
Adding loading='lazy' to the hero image at the top of the page will:
Why is the YouTube facade pattern more effective than adding loading='lazy' to a YouTube iframe?
On a page with 40 product images (60KB each), lazy loading all but the 8 above-fold images saves approximately how much initial bandwidth?
| Only the first slide is visible |
Never lazy load:
| Content | Why |
|---|---|
| Hero image / LCP element | Delays Largest Contentful Paint, hurts Core Web Vitals |
| Above-fold product images | Users see a blank box instead of content |
| Logo and navigation icons | Visible immediately, tiny files |
| Critical fonts | Text becomes invisible or shifts |
The LCP trap: Google's Largest Contentful Paint metric measures when the largest visible element renders. If you lazy-load the hero image, the browser does not start fetching it until the IntersectionObserver triggers. LCP jumps from 1.5s to 3s+. Chrome's Lighthouse explicitly warns against lazy loading above-fold images.
YouTube facade pattern: Instead of a full YouTube iframe (which loads 500KB+ of player JavaScript), render a static thumbnail with a play button. When the user clicks play, replace the thumbnail with the actual iframe. This saves 500KB+ for every embedded video that users do not play.
The rule that resolves most of it: load what is visible eagerly and reserve space for everything else. Lazy loading is a tool for the long tail of a page, not a default to apply everywhere.