For years, adding lazy loading images to a site meant shipping a JavaScript library, wiring up an IntersectionObserver, swapping data-src for src, and hoping nothing broke when JavaScript failed to load. That era is over. Every browser your visitors are realistically using in 2026 supports the native loading attribute in HTML, which means you can delete the library, ship less JavaScript, and often improve your Largest Contentful Paint at the same time.
This guide is a practical walkthrough: the exact syntax, when to use lazy versus eager, how to pair it with width and height so you do not trade one performance problem for a layout shift, and how to prove the improvement in Lighthouse.
What the loading attribute actually does
The loading attribute tells the browser whether to fetch an image immediately or defer it until the user is close to scrolling it into view. It works on <img> and <iframe> elements, and it requires zero JavaScript.
<img src="/images/gallery-01.jpg" alt="Studio workspace" loading="lazy" width="1200" height="800">
That single line replaces most of what a lazy loading library was doing for you.
The accepted values
| Value | Behaviour | Use it for |
|---|---|---|
lazy |
Defers the fetch until the image is within a browser-calculated distance of the viewport | Everything below the fold: galleries, article body images, footers, long product grids |
eager |
Loads immediately, regardless of position on the page | Hero images, logos, above-the-fold banners, your LCP candidate |
| omitted | Default browser behaviour, equivalent to eager | Fine for above-the-fold images, but being explicit documents your intent |
Important nuance: loading="lazy" does not mean “load when the image enters the viewport”. Browsers deliberately start the fetch before the image is visible, using a scroll distance threshold that varies with connection quality. On a fast connection Chrome typically starts loading images roughly a viewport or so ahead; on slow connections it starts much earlier. The goal is that the image is already decoded by the time you scroll to it, so users never see an empty box.

Browser support in 2026
Native image lazy loading is now part of the widely available baseline. There is no meaningful support gap left to justify a polyfill.
| Browser | Supported since | Notes |
|---|---|---|
| Chrome | 77 | First implementation, also covers iframes |
| Edge | 79 | Chromium based, identical behaviour |
| Firefox | 75 | Images first, iframes added later |
| Safari (macOS) | 15.4 | Shipped enabled by default |
| Safari (iOS) | 15.4 | Same engine, same behaviour |
| Samsung Internet / Opera | 12 / 64 | Chromium based |
Global support sits comfortably above 96 percent of tracked traffic, and the failure mode is the friendliest one possible: an unsupported browser simply ignores the attribute and loads the image normally. No broken images, no blank placeholders, no JavaScript dependency. Compare that with a JS library, where a script error means every image on the page stays empty. There is more on it in Lazy load images for performance.
If you want to feature-detect anyway, for example to decide whether to keep a fallback around during a migration:
if ('loading' in HTMLImageElement.prototype) {
// Native lazy loading is available
}

When to use eager instead of lazy
This is where most teams get it wrong. The instinct after discovering the attribute is to add loading="lazy" to every image on the site with a find and replace. Do not do that. Lazy loading your hero image delays the exact resource Google measures for Largest Contentful Paint, because the browser has to run layout before it decides the image is needed, instead of discovering it in the preload scanner. w3schools.com makes the same point with more data.
The rule of thumb
- Identify every image that can appear in the initial viewport on mobile (the narrowest, tallest layout is what matters).
- Give those images
loading="eager", or simply leave the attribute off. - Give the single most important above-the-fold image
fetchpriority="high". - Everything else gets
loading="lazy".
<!-- Hero: the LCP element -->
<img src="/images/hero.webp"
alt="Team reviewing a design system"
width="1600" height="900"
loading="eager"
fetchpriority="high"
decoding="async">
<!-- Anything further down the page -->
<img src="/images/case-study-03.webp"
alt="Dashboard redesign before and after"
width="1200" height="675"
loading="lazy"
decoding="async">
What about fetchpriority?
fetchpriority="high" is the companion attribute that tells the browser to move that image to the front of the network queue. It is supported across Chromium browsers, Safari and Firefox, and it is frequently worth 200 to 500 milliseconds of LCP on image-led landing pages. Use it on one image per page. If you mark five images as high priority, you have effectively marked none of them.
The mirror image trick also exists: fetchpriority="low" on carousel slides two through six, so slide one wins the race.
Pair it with width and height, or you will trade LCP for CLS
Lazy loaded images arrive late by design. If the browser does not know how much space to reserve, the surrounding content jumps when the image finally lands. That is Cumulative Layout Shift, and it is a Core Web Vital you can fail just as easily as LCP.
The fix is old-fashioned and takes five seconds: put the intrinsic pixel dimensions in the HTML. Modern browsers convert them into an implicit aspect-ratio, reserve the box before the file arrives, and the page stays still.
<img src="/images/team.webp" alt="Our team" width="800" height="533" loading="lazy">
Two things people get confused about:
- These are not CSS sizes. They are the natural dimensions of the file. Your CSS can still do
max-width: 100%; height: auto;and the image will scale responsively while keeping the reserved ratio. - Always include
height: autoin your stylesheet when you set a fluid width, otherwise you will squash the image back to the literal HTML height.
img {
max-width: 100%;
height: auto;
}
Responsive images and srcset
Lazy loading composes perfectly with srcset and <picture>. Put the attributes on the <img> element, never on <source>:
<picture>
<source type="image/avif" srcset="/img/shot-800.avif 800w, /img/shot-1600.avif 1600w" sizes="(max-width: 700px) 100vw, 700px">
<source type="image/webp" srcset="/img/shot-800.webp 800w, /img/shot-1600.webp 1600w" sizes="(max-width: 700px) 100vw, 700px">
<img src="/img/shot-800.jpg" alt="Product screenshot"
width="1600" height="1000" loading="lazy" decoding="async">
</picture>

Deleting your JavaScript lazy loading library
Here is the migration, in order. Budget an hour for a medium site.
- Audit the markup. Search your templates for
data-src,data-srcset,lazyload,lozad,blazyorlazysizes. - Restore real src attributes. Rename
data-srcback tosrcanddata-srcsetback tosrcset. This alone improves SEO robustness, because crawlers see genuine image URLs. - Add
loading="lazy"to every image except the above-the-fold ones. - Add
widthandheightto every image that does not already have them. - Remove the library script tag, the CSS classes it required, and the placeholder base64 pixels it used.
- Remove the noscript fallbacks. They exist only because JS lazy loading breaks without JavaScript. Native lazy loading does not.
- Re-test on a real mobile device with throttling, then in Lighthouse.
Native versus library, side by side
| Criteria | Native loading=”lazy” | JavaScript library |
|---|---|---|
| Payload | 0 KB | 2 KB to 20 KB gzipped, plus parse and execution |
| Works without JS | Yes | No |
| Main thread cost | None, handled by the browser | Observer callbacks compete with hydration |
| Crawlable image URLs | Always in the src attribute | Often hidden in data attributes |
| Ctrl+F / find in page | Triggers loading correctly | Frequently broken |
| Fade-in animations | Needs a small CSS or JS addition | Built in |
| CSS background images | Not covered | Usually covered |
The last two rows are the only honest reasons to keep a library, and both have native workarounds discussed below.
What about CSS background images and iframes?
The loading attribute is HTML only, so a background-image in CSS is not covered. Two options:
- Convert to a real
<img>withobject-fit: cover. This is almost always the better choice: it is more accessible, more crawlable and lazy-loadable. - Scope the background inside a media query so it is only fetched at the breakpoint that needs it, which the browser handles natively.
Iframes use the exact same attribute, and it is a huge win for embedded video and maps:
<iframe src="https://www.youtube.com/embed/VIDEO_ID"
title="Product demo"
width="560" height="315"
loading="lazy"
allowfullscreen></iframe>

Measuring the LCP improvement in Lighthouse
Do not take the win on faith. Measure it before and after so you have a number to show.
- Open the page in a Chrome Incognito window so extensions do not pollute the trace.
- Open DevTools, go to the Lighthouse panel, choose Mobile and the Performance category.
- Run the audit three times and keep the median. Single runs are noisy.
- Record the LCP value, the Total Blocking Time, and the score.
- Ship the change, then repeat the exact same procedure.
The audits to watch
| Lighthouse audit | What it is telling you |
|---|---|
| Defer offscreen images | You still have below-the-fold images without loading="lazy" |
| Largest Contentful Paint image was lazily loaded | You lazy loaded your hero. Switch it to eager immediately |
| Image elements do not have explicit width and height | Missing dimensions, CLS risk |
| Reduce unused JavaScript | Should shrink once the library is gone |
| Avoid large layout shifts | Confirms your width/height work paid off |
Lighthouse is lab data. For the real verdict, check field data in the Chrome User Experience Report through Search Console or PageSpeed Insights around 28 days after deployment.
Typical results
On content-heavy pages we have migrated, the pattern is consistent:
- Initial page weight drops sharply on long pages, often by 60 to 80 percent, because dozens of images are never requested for users who bounce at the fold.
- LCP improves modestly from the removed library, and significantly once
fetchpriority="high"is applied to the hero. - Total Blocking Time improves because the observer code no longer runs during hydration.
- CLS stays flat or improves, provided you did step four of the migration.
Common mistakes to avoid
- Lazy loading everything. The single most damaging mistake. Above-the-fold images must be eager.
- Forgetting mobile viewports. An image below the fold on desktop can be above the fold on a phone. Test at 360px wide.
- Keeping the library “just in case”. Running both means the library hides the real
src, and native lazy loading never engages. - Missing dimensions. Great LCP with terrible CLS is not a win.
- Lazy loading images in the first carousel slide. They are visible on load, so they are eager by definition.
- Assuming it fixes oversized files. Lazy loading defers bytes, it does not shrink them. Serve AVIF or WebP at sensible dimensions as well.

A production-ready template
<!-- Above the fold -->
<img src="/img/hero-1600.webp"
srcset="/img/hero-800.webp 800w, /img/hero-1600.webp 1600w"
sizes="100vw"
alt="Descriptive alt text"
width="1600" height="900"
loading="eager" fetchpriority="high" decoding="async">
<!-- Below the fold -->
<img src="/img/section-1200.webp"
srcset="/img/section-600.webp 600w, /img/section-1200.webp 1200w"
sizes="(max-width: 768px) 100vw, 600px"
alt="Descriptive alt text"
width="1200" height="800"
loading="lazy" decoding="async">
FAQ
How do I lazy load images in HTML?
Add loading="lazy" to any <img> tag that sits below the fold, and include width and height attributes so the browser reserves the space. No JavaScript, no library, no build step.
What does lazy loading images actually do?
It delays the network request for off-screen images until the user scrolls near them. That reduces initial page weight, frees bandwidth for critical resources such as your hero image and fonts, and usually improves Largest Contentful Paint and time to interactive.
Does loading=”lazy” hurt SEO?
No, when implemented natively. Googlebot supports the attribute and lazily loaded images are indexed normally because the real URL stays in the src attribute. The SEO risk comes from JavaScript libraries that hide URLs in data-src. Migrating to native lazy loading is generally an SEO improvement.
Why are my images still loading all at once?
Usually one of four reasons: they are all within the browser’s loading threshold on a short page, an old JS library is still overwriting the src, the images are CSS backgrounds rather than <img> elements, or you are testing on a fast connection where the browser preloads aggressively. Throttle to Slow 4G in DevTools to see the difference clearly.
Should I use loading=”lazy” on my logo?
No. Logos are almost always in the header and therefore visible on load. Lazy loading them delays a visible element for no benefit.
Can I still animate images as they appear?
Yes. Use CSS to fade in on the load event, or use a CSS scroll-driven animation. A handful of lines beats a full library.
Does the loading attribute work with the picture element?
Yes. Place loading="lazy" on the inner <img>, which is the element the browser actually renders. It applies to whichever <source> is selected.
The takeaway
Native lazy loading is the rare optimisation that removes code, removes a dependency, removes a failure mode, and improves your Core Web Vitals at the same time. The recipe is short: eager plus fetchpriority for the hero, lazy for everything below the fold, width and height on every single image, then confirm the LCP delta in Lighthouse.
If you would like a second pair of eyes on your Core Web Vitals or an image delivery pipeline that serves modern formats at the right dimensions, the team at PixelFabs runs performance audits that start exactly here.