HTMLLazy Loading Images

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

HTML
<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

Note
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.

Tip
Because unsupported browsers silently ignore 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.

HTML
<!-- 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">
Warning
A common performance regression is adding 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.

HTML
<img class="lazy" data-src="photo.jpg" src="placeholder.svg" alt="Team offsite photo" width="800" height="500">

JS
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
Tip
Reach for Intersection Observer only when you need behavior the native attribute doesn't offer — such as loading non-<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

HTML
<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/height so the layout does not shift as images load in.

Key Takeaways
  1. loading="lazy" is a native, zero-JS way to defer off-screen image downloads.

  2. It is supported broadly; unsupported browsers just ignore it and load eagerly.

  3. Never lazy-load the image that is likely your Largest Contentful Paint candidate — leave it eager and consider fetchpriority="high".

  4. Intersection Observer remains useful for advanced cases: non-img lazy content, custom thresholds, or visibility-based analytics.

  5. Pair lazy loading with width/height attributes to avoid layout shift.