Lazy-Loading Images Without Hurting LCP
loading="lazy" is one attribute that can either save a page megabytes or cost it half a second of Largest Contentful Paint. Applied to images far down an article, it stops the browser downloading pictures nobody scrolls to. Applied to the hero image, it tells the browser to wait until layout is done before even requesting the most important file on the page. Because many image components and plugins make lazy the default, the second case is common on static sites.
This guide shows how lazy loading delays the LCP image, how to decide which images are eager, and how to wire that decision into Astro, Hugo and Eleventy templates so authors do not have to think about it. It is part of Image Optimization Pipelines in Astro.
Prerequisites
- A page template with a hero or lead image.
- Field LCP data with element attribution, or a Lighthouse report that names the LCP element.
- Access to your image component or shortcode.
Why a Lazy Hero Is Slow
With a normal <img>, the browser's preload scanner sees the tag while the HTML is still streaming and requests the image immediately, often before CSS has finished loading. With loading="lazy", the browser must know where the image sits relative to the viewport, which it only knows after the CSS is loaded and layout is complete. The image request is therefore delayed until after render-blocking CSS, and it starts at a lower priority.
In Chrome's field data, pages that lazy-load their LCP image have a noticeably worse LCP at the 75th percentile than pages that do not. Lighthouse flags it with the audit "Largest Contentful Paint image was lazily loaded".
Deciding Which Images Are Eager
The rule is simple to state: any image that can be in the initial viewport on any supported screen size is eager; everything else is lazy. In practice, templates make it predictable:
- Article template: the hero or lead image is eager with
fetchpriority="high". Inline content images are lazy. - Listing and index pages: the first row of cards is eager, later rows lazy. On a three-column grid that means the first three thumbnails.
- Homepage: the hero is eager with high priority; logos and feature images further down are lazy.
- Header logo: if it is an
<img>, eager but normal priority. An inline SVG avoids the question entirely.
Use fetchpriority="high" on one image only. It raises that image above other images and non-critical scripts in the browser's queue. Marking several images high makes them compete with each other and with CSS; see Optimizing LCP on Astro with Priority Hints.
Wiring It Into Templates
The goal is that authors never set loading by hand. The template knows which image is the hero and passes the right attributes.
Astro. The <Image> and <Picture> components default to loading="lazy" and decoding="async". Override on the hero:
<Image src={hero} alt={heroAlt} widths={[640, 960, 1280]}
sizes="100vw" loading="eager" fetchpriority="high" />
For listing pages, pass the index into the card component and set loading={index < 3 ? 'eager' : 'lazy'}.
Hugo. In a render hook for Markdown images, layouts/_default/_markup/render-image.html, default to lazy. In the single template, render the hero with loading="eager" fetchpriority="high" explicitly. For list pages, use {{ if lt $index 3 }}eager{{ else }}lazy{{ end }} inside range.
Eleventy. Set loading: 'lazy' in the image transform's default attributes, and put loading="eager" fetchpriority="high" directly on the hero <img> in the layout; the transform keeps attributes that are already present. See Responsive Images with srcset in Eleventy.
Lazy Loading and Layout Shift
Lazy images must reserve space, or they cause layout shift when they load in as the reader scrolls. Every image component mentioned here writes width and height, which gives the browser the aspect ratio before the file arrives. For CSS background images and iframes, set aspect-ratio in CSS. Iframes also accept loading="lazy", which is the easiest win for embedded videos and maps; for YouTube, a click-to-load facade is better still — see Lazy-Loading YouTube Embeds on Static Sites.
Placeholders and Blur-Up Effects
Low-quality placeholders — a tiny blurred version of the image shown until the real file loads — are popular, and they interact with lazy loading in two ways worth knowing.
First, a placeholder does not count as the LCP element's final paint unless it is the same element at the same size. If the placeholder is a CSS background and the real image is an <img> that fades in, LCP is recorded when the real image paints. A fade-in animation that starts at zero opacity delays LCP further, because an element with zero opacity is not a candidate. Keep fades off the hero, or start them at a visible opacity.
Second, placeholder libraries that swap data-src into src with JavaScript are invisible to the preload scanner, so they behave like the worst possible lazy loading even when the image is above the fold. If you want a placeholder on the hero, use a CSS background-image with a small inline data URI on the wrapper and a normal eager <img> inside it.
For images below the fold, placeholders are harmless and improve perceived speed on slow connections. The dominant colour of the image as a plain background-color is the cheapest version: a few bytes per image, no extra request and no JavaScript.
Auditing a Whole Site
Checking templates by eye misses edge cases such as a page type whose first content block is an image. Automate it: run Lighthouse or a Playwright script across one URL per template and fail if the LCP element is an <img> with loading="lazy". In field data, the web-vitals attribution build reports the LCP element and its URL; group by template and check whether any template's LCP image has a long "resource load delay", which is the signature of lazy loading. The four LCP subparts are explained in Measuring LCP Subparts with DevTools.
Measured Impact
A 1,600-page Astro blog used the default <Image> component everywhere, so every hero was lazy. Setting the hero to eager with high fetch priority in the article layout — a two-line change — cut mobile LCP p75 from 2.8 to 2.1 seconds and moved the article template from "needs improvement" to "good" in Search Console. Image bytes per view rose by 8 KB on average because the hero was now fetched even when the reader left instantly.
Pitfalls & Rollback
- A global
lazydefault with no per-template override. The most common cause; audit the layouts that render heroes. - Lazy loading the logo in a sticky header. It is always in view; load it eagerly or inline it.
- Carousels. Only the first slide is visible; make it eager and lazy-load the rest.
- JavaScript lazy loaders with
data-src. They hide the image from the preload scanner entirely; replace them with native loading. - Rollback: revert the template change; nothing else depends on the attribute.
Conclusion
Lazy loading is a saving for images below the fold and a delay for images above it. Decide per template, not per author: heroes and first-row images eager, one image with fetchpriority="high", everything else lazy with explicit dimensions. Wire those rules into the Astro, Hugo or Eleventy templates and check the LCP element in CI, and the attribute will save bytes without costing LCP.
FAQ
Does loading lazy hurt LCP?
Only when it is applied to the image that becomes the LCP element, or to images above the fold. A lazy image is not requested until layout is complete and the browser knows it is near the viewport, which can add several hundred milliseconds to its load. Images below the fold are safe to lazy-load.
How many images should be eager on a page?
Usually the first one or two images that can appear in the initial viewport, including the hero and a logo if it is an img element. Everything that starts below the fold on the smallest supported screen can be lazy.
Should I use fetchpriority high as well as eager?
For the single LCP image, yes. fetchpriority high moves it ahead of other images and scripts in the request queue. Use it on one image per page only; marking several images high priority cancels the benefit.
Is JavaScript lazy loading still needed?
Rarely. Native loading lazy is supported in all current browsers for images and iframes. JavaScript libraries are only needed for effects such as blur-up placeholders, and they should never be applied to the LCP image.
Related
- Parent: Image Optimization Pipelines in Astro — the full image pipeline.
- Reducing LCP from Hero Images on Static Sites — the rest of the hero checklist.
- Optimizing LCP on Astro with Priority Hints — fetchpriority in depth.
- Responsive Images with srcset in Eleventy — setting defaults in the transform.
- Measuring LCP Subparts with DevTools — spotting load delay.