When loading="lazy" is not deferring anything, the cause is almost always one of three: it is on an element that does not support it, it was never written at all, or it is on an image that must not be deferred. On <img> and <iframe> the attribute is supported by every modern browser as of 2026 (Baseline), so browser support is rarely what is wrong.
This article separates where the attribute works from where it does not, and covers the downsides that come with it—you cannot tune the distance at which fetching begins, and it cannot drive effects or measurement—along with how fetchpriority and decoding fit alongside it.
Sponsored
loading=”lazy” works on img and iframe only
Short version: the loading attribute exists on <img> and <iframe>, and nowhere else. 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. An attribute on <source> is silently ignored, which makes this the first place to look when nothing is being deferred.
<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 of lazy loading.
Sponsored
Without width and height the layout jumps
Leave them out and you get layout shift (CLS). An unloaded image has zero height, so everything below it jumps down the moment the image arrives. If deferral is clearly working but the page “breaks” visually, this is the cause.
<!-- 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, which makes this the most reliable approach.
img {
max-width: 100%;
height: auto;
}
Do not put lazy on above-the-fold images
It will degrade LCP (Largest Contentful Paint). 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:
<!-- main visual at the top of the page -->
<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 you only see after scrolling |
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 in bulk, check whether it can exclude the first one. Fixing just that often produces a visible LCP improvement.
Sponsored
The downsides and limits of loading=”lazy”
In exchange for being a single attribute, it gives you no fine-grained control at all. The downsides come down to two.
You cannot tune the distance at which loading starts
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 yourself. If you need images to start earlier, your options are:
- Do not mark it
lazyat all (it is an important image) - Use Intersection Observer and set
rootMarginyourself
It cannot do background images, effects, or measurement
The loading attribute does one thing: fetch the resource when it gets close to the screen. It cannot do any of the following.
- Lazy-loading a CSS background image: there is no element to put the attribute on
- Fading an image in once it has loaded: you need a
loadevent and a class of your own - Counting how often an element entered the viewport
- Initialising an ad or map script just before it is displayed
Those implementations, and pre-fetching with rootMargin, are covered in lazy-loading background images and embeds with the Intersection Observer API.
How to check that it is working
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
widthandheightpresent everywhere? - Transfer size: in DevTools’ Network panel, did the number of image requests on initial render drop?
To confirm the “I wrote the attribute and nothing happens” case, open the Network panel, keep the page scrolled to the top, and watch whether requests for off-screen images fire anyway. If they do, the attribute is not reaching the browser—written on <source>, stripped during the build, and so on.
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
loadingworks on<img>and<iframe>only—not<video>or<source>- Inside
<picture>, write it on the inner<img>, not on<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
widthandheightto prevent CLS - Never lazy-load the hero image; use
fetchpriority="high"instead - The downsides: the trigger distance is not configurable, and background images, effects, and measurement are out of scope. That is Intersection Observer’s job
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.