Tutorial · images · Published 2026-08-15 · 3 min read
Lazy loading images without breaking LCP
Lazy load images with the loading attribute, decode async, keep the LCP image eager, and cover no-JS edge cases without hurting LCP.
How lazy loading works
Native lazy loading defers the fetch of an image until the browser decides the image is close to entering the viewport. The attribute is just loading="lazy" on the img tag. The browser sets its own threshold, usually a configurable distance ahead of the viewport, and starts the fetch when that run is close. Images below the fold save bytes on the initial page load.
Important: the attribute is native HTML, not a library. That means it also works when JavaScript fails, which the fallback section below relies on.
Keep the LCP image eager
The rule that determines everything else: never lazy-load the LCP image. LCP is the Largest Contentful Paint, the point when the largest visible block finishes rendering. On a marketing page that is almost always the hero image. If the hero has loading="lazy", the browser delays its fetch until the viewport is close, which pushes LCP well past 2.5 seconds (the good threshold per LCP optimisation). The image above the fold must load eagerly.
Practical rule of thumb:
- Any
imgthat appears in the first loaded screen keeps default eager loading. Do not addloading="lazy"to it. - Images below the fold may take
loading="lazy". - Test the change. The web performance audit explains the measurement loop.
Apply it in practice
For each page, work downstream from the hero:
<img src="/hero.webp" width="1600" height="900" alt="..." fetchpriority="high">
<img src="/gallery-1.webp" loading="lazy" decoding="async" width="800" height="500" alt="...">
Notes on the second tag:
loading="lazy"defers the gallery image.decoding="async"tells the browser it may continue painting while the image decodes, so a low CPU device does not stall the render forgallery-1.widthandheighton both tags reserve layout space, which stops the page from shifting when the lazy image loads. Without them, lazy loading itself can cause CLS.
Fallbacks and edge cases
No JavaScript visitors
Native lazy loading is HTML; a visitor with JavaScript disabled still gets lazy loading. But a shop or analytics must not decide your lazy-loading strategy, because it applies regardless. The one assumption to avoid is that scripts like IntersectionObserver are required. They are not for the lazy part.
iframes
loading="lazy" works on iframes too, which is useful for embeds, but it does not shrink the download. If a below-the-fold iframe is heavy, keep it lazy.
Errors and blank states
A lazy image reads its src only when the browser decides to fetch it, which can be long after first paint. An image that fails to load leaves an empty reserved box unless you give it alt text, which also helps screen readers. The reserved box from width and height prevents layout shift whether or not the image arrives.
Screen readers and crawlers
Native lazy loading is safe for crawl: Google reads the full HTML and renders with the same lazy mechanics a browser uses, so an image with a correct src is visible to the index even when it is below the fold. Keep the src in the tag and do not move the real URL into data-* attributes, because an empty src pattern is exactly what some crawlers ignore.
Verify the result
Check in devtools that below-the-fold images do not network-request until you scroll to them, and re-run Lighthouse to confirm LCP did not regress. The two belt-and-suspenders metrics are hero LCP and total bytes saved, both described in the LCP optimisation expectations.