Because you almost certainly lazy loaded the hero image. Lazy loading tells the browser to wait before fetching an image. When you apply it to the biggest image at the top of the page, you have told the browser to delay the exact thing the visitor is waiting to see. That wrecks your Largest Contentful Paint.
This is the most common performance mistake we find when we take over an underperforming site. Someone read that lazy loading is good for speed, switched it on for every image site wide, and watched their scores get worse. The advice was not wrong. The application of it was.
Lazy loading is a genuinely useful tool. It is also one of the few performance techniques that can actively harm you when applied without thought, which is why it deserves a proper explanation rather than a checkbox.
Lazy loading defers fetching an image until the visitor scrolls near it. In modern browsers you get it by adding the loading attribute to an image tag with the value lazy. MDN documents two values, lazy and eager, and eager is the default. No JavaScript library is needed any more.
The logic is simple. A page with forty images below the fold does not need forty image downloads before the visitor sees anything. Deferring them frees bandwidth for the things on screen and cuts the total weight of the initial load, which is why it helps most pages most of the time.
Before browsers supported this natively, everyone used a JavaScript library that watched scroll position or used IntersectionObserver, usually by hiding the real image path in a data-src attribute. Those libraries still exist in old codebases and are still worth removing. The native version is faster, smaller, and does not break when a script fails.
The HTTP Archive Web Almanac 2025 Performance chapter shows the shift happening. Native lazy loading rose to 10.4% of mobile pages and 11.5% of desktop pages, while custom data-src approaches fell to 5.9% on mobile. The old way is on the way out, and that is a good thing.
When it is applied to anything visible on first load, and especially to the LCP element. Google's own web.dev guidance is direct about this. It says not to lazy load images likely to be in the viewport when the page loads, especially LCP images, and to let those load eagerly so they are available right away.
The reason is mechanical rather than a matter of opinion. Your Largest Contentful Paint is usually the hero image. If you defer it, the browser will not even request it until layout has run and it knows the image is near the viewport. You have inserted a delay into the single metric Core Web Vitals cares most about.
The same applies to a logo, a headshot next to a headline, or the first card in a grid. Anything a visitor sees without scrolling should load eagerly. Everything below that line is fair game.
We treat this as a hard rule on every build. The rule is not lazy load images. The rule is lazy load images below the fold, and it is the second half that people drop. If you want the fuller picture on the metric itself, our guide to fixing Largest Contentful Paint works through the other causes too.
Roughly one in six. The HTTP Archive Web Almanac 2025 Performance chapter reports that about 16% to 17% of pages lazy load their LCP image, a figure that has held steady since 2024. That is millions of pages actively delaying their own main content, and the number is not improving.
What makes that figure striking is how well known the guidance is. This is not an obscure edge case. Google has published the warning for years, Lighthouse flags it, and PageSpeed Insights calls it out by name. The problem is that the fix lives in a template, and templates get set up once and never revisited.
The Almanac also notes that native lazy loading now accounts for a larger share of LCP lazy loading than the custom approaches do. That is slightly awkward. The modern, recommended technique has become the more common way to make this mistake, simply because it is so easy to add one attribute to every image in a component.
Across our own projects we hold an average PageSpeed score of 98, and a meaningful part of that is just not making this one mistake. It is not clever work. It is discipline about which images get the attribute.
Further than most people assume, and the distance changes with connection speed. Chrome starts fetching a lazy image when it comes within 1,250px of the viewport on a 4G connection, and within 2,500px on 3G or slower. Slower connections get a bigger head start, because they need more time.
This matters because it explains why lazy loading rarely produces the visible pop-in people fear. The browser is not waiting until the image scrolls into view. It is loading well before that, and the slower your visitor's connection, the earlier it starts.
Chrome's own testing backs that up. Google reports that on 4G, 97.5% of lazy loaded images were fully loaded within 10ms of becoming visible, and even on slow 2G, 92.6% were fully loaded within 10ms. In practice, correctly applied lazy loading is invisible to the visitor.
So the fear that lazy loading will make your gallery feel janky is mostly unfounded. The real risk is the opposite one, which is being too aggressive at the top of the page rather than too slow further down.
Yes, and it pairs with lazy loading rather than replacing it. The fetchpriority attribute takes high, low, or auto, and setting it to high on your LCP image tells the browser to prioritise it immediately instead of waiting for layout. Lazy loading defers the unimportant images. Fetch priority promotes the important one.
Google explains why this is needed. Images inside the viewport typically start at a low priority, and Chrome only boosts them after layout completes and it discovers they are on screen. That discovery step adds a real delay to exactly the image you most wanted early. Declaring high priority in the markup skips the wait.
The reported gains are not small. Google's guidance cites Google Flights improving LCP from 2.6 seconds to 1.9 seconds by applying fetch priority to an LCP background image, a 0.7 second gain from one attribute. It also cites the Oodle app cutting overall page load time by 2 seconds by prioritising only the first carousel image.
Our default pattern on a hero is therefore two decisions, not one. The hero image gets eager loading and high fetch priority. Everything below the fold gets lazy loading and no priority hint at all.
It can, and the fix is always the same. Always set explicit width and height attributes on every image. Without them the browser does not know how much space to reserve, so content jumps when the image arrives. MDN is blunt that an unloaded image has a width and height of 0.
MDN raises a second and stranger consequence of that zero size. A lazy loaded image will never load if it does not intersect a visible part of an element, because at zero by zero it may never come near the viewport at all. An image inside a collapsed or zero height container can simply never appear.
The user experience cost is what MDN describes as content in the viewport reflowing in the middle of reading. That is the worst kind of layout shift, because it happens after the reader has already committed attention to a line of text. Reserved space prevents all of it. We covered the wider version of this problem in our guide to fixing Cumulative Layout Shift.
Modern CSS makes this easy. Set the width and height attributes to the image's real pixel dimensions, then let CSS handle the responsive sizing. The attributes give the browser an aspect ratio to reserve, and the stylesheet still controls how it displays.
No, and this is the detail almost nobody knows. MDN states that loading is only deferred when JavaScript is enabled, and that this is a deliberate anti tracking measure. With scripting disabled, lazy loading would let a site infer a visitor's scroll position, so browsers switch it off entirely.
The practical effect is reassuring rather than alarming. A visitor with JavaScript disabled gets every image loaded eagerly. Nothing breaks and nothing goes missing. The page is heavier, but it is complete.
This matters for crawlers too. A crawler that does not execute JavaScript sees your image tags with real src values in the HTML, because native lazy loading never removes the source path. That is a genuine advantage over the old data-src libraries, which hid the real path from anything that did not run scripts. It is the same reason we care about how images are served in the first place.
Split your images into two groups and treat them differently. Anything visible without scrolling gets eager loading, explicit dimensions, and high fetch priority on the LCP image. Everything else gets lazy loading and explicit dimensions. Then verify with Lighthouse rather than trusting the template.
The verification step is the one people skip. Run PageSpeed Insights on the page and look for the warning about a lazy loaded LCP image. It names the offending element directly, so you do not have to guess. If the warning is absent and your LCP is under 2.5 seconds, you are done.
The awkward case is a CMS collection where the same component renders both the first card and the fiftieth. The first one is above the fold and the rest are not, but they share one template. The clean solution is to pass the index into the component and set loading eagerly only for the first item or two, which most modern site builders support.
Whatever tool you build in, the same principle holds. Most modern site builders expose a per image loading setting, and most of them pick a default that suits the common case rather than your hero. We set the hero explicitly on every build instead of trusting a default, because that one image decides the score.
Open your homepage in PageSpeed Insights and check whether your LCP image is lazy loaded. That single check catches the mistake that costs the most. If it flags, change that one image to eager, add high fetch priority, and re-run. It is often a five minute fix with a visible result.
Then audit your templates rather than your pages. The mistake almost always lives in a component that renders many times, so fixing it once fixes it everywhere, and missing it once misses it everywhere. Page by page auditing on a CMS driven site is wasted effort.
If your scores are stuck and you cannot work out which image is the culprit, or your site builder is not giving you control over the attribute, we are happy to look at it with you. This is the kind of thing that is quick to diagnose when you have seen it a hundred times. Come say hello at phoenix.studio.
Tell us where you want to go. We'll tell you how we'd get you there.