Lazy Loading Images
Most web pages contain more images than fit in the initial viewport. Downloading all of them immediately wastes bandwidth and delays the images the user can actually see. Lazy loading defers off-screen images until the user is about to scroll them into view.
Browsers now support this natively with a single HTML attribute — no JavaScript library required for the common case.
The loading Attribute
<img src="chart-q3-revenue.png" alt="Q3 revenue chart" loading="lazy" width="800" height="400">
Value | Behavior |
|---|---|
lazy | Defer loading until the image is near the viewport |
eager | Load immediately (the default browser behavior) |
(omitted) | Same as eager — images load immediately by default |
loading="lazy" works on both <img> and <iframe>. The browser typically starts loading the resource once it is within a small distance of the viewport (the exact threshold is browser-defined and not standardized), so images are usually ready by the time the user actually scrolls to them.Native Browser Support
Native loading="lazy" is supported in all major modern browsers — Chrome, Edge, Firefox, and
Safari have all shipped support for years now. Unsupported browsers simply ignore the attribute
and load the image normally, which makes it a safe progressive enhancement with zero fallback
code required.
loading="lazy", there's no harm in adding it broadly — worst case, older browsers just load the image eagerly, exactly as they would have without the attribute.Don't Lazy-Load the LCP Image
Largest Contentful Paint (LCP) measures how long it takes the biggest visible element — often a hero image — to render. Lazy-loading that image adds a delay: the browser has to wait for layout and scroll position before it even starts the network request, which makes LCP worse, not better.
<!-- Above-the-fold hero image: load it eagerly, and hint high priority --> <img src="hero.jpg" alt="New product lineup" loading="eager" fetchpriority="high" width="1600" height="900" > <!-- Images further down the page: safe to lazy-load --> <img src="feature-1.jpg" alt="Feature: dashboard view" loading="lazy" width="800" height="500"> <img src="feature-2.jpg" alt="Feature: reporting view" loading="lazy" width="800" height="500">
loading="lazy" indiscriminately to every image on a page, including the hero image the Largest Contentful Paint metric is measuring. Always leave above-the-fold, LCP-candidate images as loading="eager" (or simply omit the attribute).fetchpriority as a Complement
fetchpriority="high" tells the browser's preload scanner to fetch that resource earlier than
its position in the document would normally suggest — useful for the hero/LCP image even when
it appears after some markup (like a header) in the HTML source.
Intersection Observer: The JS Fallback
Before native lazy loading existed, and still occasionally useful for finer control (custom
loading thresholds, placeholder swapping, tracking analytics on view), developers used the
Intersection Observer API to detect when an image's placeholder scrolls into view, then
swap in the real src.
<img class="lazy" data-src="photo.jpg" src="placeholder.svg" alt="Team offsite photo" width="800" height="500">
const images = document.querySelectorAll('img.lazy');
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
obs.unobserve(img); // stop watching once loaded
}
});
}, {
rootMargin: '200px', // start loading 200px before it enters the viewport
});
images.forEach((img) => observer.observe(img));Scroll position: image is 250px below the viewport --> not intersecting yet, no request sent Scroll position: image is 150px below the viewport --> within rootMargin, request sent, real src swapped in
<img> content (background images in CSS, custom carousels) or firing an analytics event exactly when content becomes visible. For plain <img> lazy loading, the native attribute is simpler and cheaper.Putting It Together: A Realistic Page
<body>
<header>
<img src="hero-banner.jpg" alt="Autumn sale banner" loading="eager" fetchpriority="high" width="1600" height="500">
</header>
<main>
<article>
<img src="product-1.jpg" alt="Wool sweater" loading="lazy" width="600" height="600">
<img src="product-2.jpg" alt="Leather boots" loading="lazy" width="600" height="600">
<img src="product-3.jpg" alt="Wool scarf" loading="lazy" width="600" height="600">
</article>
</main>
</body>Hero/LCP image:
loading="eager"(or omit the attribute) +fetchpriority="high".Everything below the fold:
loading="lazy".Always pair lazy loading with explicit
width/heightso the layout does not shift as images load in.
Key Takeaways
loading="lazy" is a native, zero-JS way to defer off-screen image downloads.
It is supported broadly; unsupported browsers just ignore it and load eagerly.
Never lazy-load the image that is likely your Largest Contentful Paint candidate — leave it eager and consider fetchpriority="high".
Intersection Observer remains useful for advanced cases: non-img lazy content, custom thresholds, or visibility-based analytics.
Pair lazy loading with width/height attributes to avoid layout shift.