Browse by section

Web Design 日本語

Native Lazy Loading in HTML: What Actually Works

Native lazy loading turns on by writing loading="lazy" on an <img> or <iframe>. Every modern browser supports it as of 2026 (Baseline), and you no longer need a JavaScript library.

The explanations you find online, however, mix in outdated and simply incorrect claims. This article separates what actually works from what is commonly asserted but false, and covers how fetchpriority and decoding fit alongside it.

Sponsored

Which elements support the loading attribute

Short version: only <img> and <iframe>. The values are lazy and eager (the default).

<img src="photo.jpg" width="800" height="600" alt="Sample" loading="lazy">

<iframe src="https://www.google.com/maps/embed?pb=..." width="600" height="400"
        loading="lazy" title="Store location map"></iframe>
Element loading Use instead
<img> Supported —
<iframe> Supported —
<video> Not supported preload="none" plus poster
<source> Not supported Put it on the inner <img>
CSS background image Not supported Intersection Observer
<script> Not supported defer / async / dynamic import()

Inside <picture>, write loading="lazy" on the <img>, not on <source>—the <img> is what makes the decision.

<picture>
  <source srcset="photo.avif" type="image/avif">
  <source srcset="photo.webp" type="image/webp">
  <img src="photo.jpg" width="800" height="600" alt="Sample" loading="lazy">
</picture>

“The browser lazy-loads automatically” is false

If you do not write the attribute, nothing is deferred. The default is eager.

This misconception has a real origin. In 2019 Chrome shipped a feature that automatically deferred off-screen images and iframes for Android users on Lite mode (Data Saver), and there was a loading="auto" value.

But both Lite mode and loading="auto" have been removed, and Chrome has stated there are no plans to lazy-load images automatically. As of 2026, writing loading="lazy" yourself is the only path.

Similarly, no loading attribute has ever been added to <video> or <source>. To defer a video:

<video src="movie.mp4" poster="thumbnail.jpg"
       width="640" height="360" controls preload="none"></video>

preload="none" stops the video data from downloading, and poster shows a lightweight still image. That is the video equivalent.

Sponsored

Why width and height are mandatory

Without them you get layout shift (CLS). An unloaded image has zero height, so everything below it jumps down the moment the image arrives.

<!-- Box reserved in advance -->
<img src="photo.jpg" width="800" height="600" alt="Sample" loading="lazy">

<!-- Content jumps when the image lands -->
<img src="photo.jpg" alt="Sample" loading="lazy">

Even for fluid responsive images, put the intrinsic dimensions in the HTML attributes and adjust with CSS. The browser derives the aspect ratio from width and height and reserves the space.

img {
  max-width: 100%;
  height: auto;
}

Do not put lazy on above-the-fold images

It will degrade LCP. With loading="lazy" on the main image at the top of the page, the browser does not begin fetching until layout is resolved, delaying the largest paint by hundreds of milliseconds or more.

For that image, raise the priority instead:

<img src="hero.jpg" width="1200" height="630" alt="Main visual"
     fetchpriority="high" decoding="async">
Attribute Effect When to use
loading="lazy" Defers the fetch Images below the fold
fetchpriority="high" Raises fetch priority The LCP hero image
fetchpriority="low" Lowers fetch priority Decorative images, footer banners
decoding="async" Decodes off the main thread Basically every image

The test is one question: is it visible without scrolling? If you use a plugin that adds loading="lazy" to every image, check whether it can exclude the first one. Fixing just that often produces a visible LCP improvement.

Sponsored

How far in advance does loading start?

The browser begins fetching at some distance before the viewport, and that distance is not fixed—it varies with effective connection speed. Chrome uses a shorter threshold on fast connections and a longer one on slow ones.

In other words, you cannot tune the threshold. If you need images to start earlier, your options are:

  • Do not mark it lazy at all (it is an important image)
  • Use Intersection Observer and set rootMargin yourself

The second option is covered in how to optimise lazy loading with the Intersection Observer API.

Fading images in as they load

You do not need to go back to the data-src pattern for this. Adding a class on the load event is enough.

img[loading="lazy"] {
  background-color: #f2f2f2;
  opacity: 0;
  transition: opacity .4s;
}
img[loading="lazy"].is-loaded {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  img[loading="lazy"] { opacity: 1; transition: none; }
}
document.querySelectorAll('img[loading="lazy"]').forEach((img) => {
  if (img.complete) {
    img.classList.add('is-loaded'); // catch cached images
    return;
  }
  img.addEventListener('load', () => img.classList.add('is-loaded'), { once: true });
});

The img.complete check is required. For images served from cache, load may have already fired before your script runs. Skip it and some images stay permanently transparent—an unpleasant bug to track down.

Measuring the effect

Do not set and forget. Compare before and after on three metrics:

  • LCP: did you lazy-load the hero image? Suspect this first if it got worse
  • CLS: are width and height present everywhere?
  • Transfer size: in DevTools’ Network panel, did the number of image requests on initial render drop?

Lighthouse reports both “Defer offscreen images” and “Largest Contentful Paint image was lazily loaded.” Seeing both at once is the quickest sign that something is wrong.

Summary

  • loading works on <img> and <iframe> only—not <video> or <source>
  • The default is eager. Nothing is deferred unless you write it; no browser does it automatically
  • loading="auto" and Chrome’s Lite mode auto-deferral are both gone
  • Always write width and height to prevent CLS
  • Never lazy-load the hero image; use fetchpriority="high"
  • The trigger distance is not configurable—use Intersection Observer if you need control
  • Fade-in needs only the load event plus an img.complete check

It comes down to two attributes, but deciding which images should not get them matters more than adding them. Start by reviewing the first image on the page.