A product listing page has a large hero banner and 60 product thumbnails. Without any hints, the browser discovers all 61 images while parsing the HTML and starts requesting them, so the banner the user is looking at competes for bandwidth with thumbnails three screens down. The page’s Largest Contentful Paint (LCP), the time until the biggest visible element renders, suffers.
HTML gives you three tools to fix the ordering: loading, fetchpriority and <link rel="preload">. Each one is easy to misuse, and the most common misuse makes the page slower, so it’s worth being clear about what each does.
How the browser schedules images by default
Before any hints, two mechanisms decide when images load.
The preload scanner reads ahead in the HTML while the main parser is blocked (on a synchronous script, for example) and starts fetching resources it finds in src and srcset attributes. That’s why an <img> in the HTML starts downloading very early, and why images inserted by JavaScript or referenced only from CSS start late: the scanner can’t see them.
Priorities decide what gets bandwidth first. In Chromium, images start at low priority. Once layout runs and the browser knows which images are in the viewport, those are boosted to high. Recent Chromium versions also give the first five or so large images a medium-priority head start. But that boost waits for layout, which waits for CSS, so even a visible hero can spend hundreds of milliseconds at low priority behind scripts and fonts.
loading=“lazy”: defer what’s off-screen
<img
src="/products/trail-runner-3-400.jpg"
alt="Trail Runner 3 in blue"
width="400"
height="300"
loading="lazy"
>With loading="lazy", the browser doesn’t request the image until it’s near the viewport. “Near” is generous: Chromium starts fetching when the image is within about 1250px of the viewport on fast connections and about 2500px on slow ones, so images are normally ready by the time the user scrolls to them. Lazy loading requires layout to know the position, so it never starts before layout.
On the 60-thumbnail page, lazy loading means a first visit downloads perhaps 8 to 12 images instead of 60. That saves data for users who never scroll, frees bandwidth for the hero, and lowers the page’s total weight.
One quirk: browsers ignore loading="lazy" when scripting is disabled, as a privacy measure, because lazy loading lets a site track scroll position through image requests. With scripting off, the images load eagerly.
The mistake that hurts LCP
Teams often add loading="lazy" to every image, through a CMS setting or a component default. That includes the hero, which is above the fold and probably the LCP element. Now the hero isn’t requested by the preload scanner at all; it waits for layout, then for the lazy-loading check, then starts at whatever priority it gets. On a mid-range phone this can add several hundred milliseconds to LCP, and Lighthouse reports it as “Largest Contentful Paint image was lazily loaded”.
The rule: lazy-load images that are certainly below the fold on all common viewports, and load eagerly (the default, loading="eager" or no attribute) anything that might be visible on first paint. If a component renders both in the first row and further down, give it a prop so the first few instances can opt out.
fetchpriority=“high”: move the hero up the queue
<img
src="/hero/autumn-sale-1600.jpg"
srcset="/hero/autumn-sale-800.jpg 800w, /hero/autumn-sale-1600.jpg 1600w"
sizes="100vw"
alt="Autumn sale: trail shoes from USD 89"
width="1600"
height="700"
fetchpriority="high"
>fetchpriority is a hint, with values high, low and auto. On the hero, high tells the browser to request it at high priority from the start instead of waiting for layout to discover that it’s visible. It doesn’t make the download faster; it moves the image ahead of lower-priority requests competing for the same connection. Google’s published case studies have reported LCP improvements of several hundred milliseconds on image-heavy pages from this one attribute, though your mileage depends on what else is in the queue.
Use it on one image, maybe two. If you mark ten images high, you’ve told the browser nothing, and you may push down CSS or fonts that render blocking content.
fetchpriority="low" is useful too: carousel slides two to five, which are in the HTML but hidden, can be marked low so they don’t compete with the visible first slide.
Preload: for images the scanner can’t see
<link rel="preload"> in the <head> starts a fetch before the parser reaches the body:
<link
rel="preload"
as="image"
href="/hero/autumn-sale-1600.jpg"
imagesrcset="/hero/autumn-sale-800.jpg 800w, /hero/autumn-sale-1600.jpg 1600w"
imagesizes="100vw"
fetchpriority="high"
>This is the right tool when the LCP image is a CSS background-image or is inserted by a client-side framework after JavaScript runs. In both cases the preload scanner can’t find it from an <img> tag, and the preload makes it discoverable early.
For a plain <img> already in the HTML, preloading usually adds little over fetchpriority="high", because the scanner finds the <img> almost as early. And preloading the wrong thing is costly: if the preload’s href and imagesrcset don’t match what the <img> eventually selects, the browser downloads the image twice, and Chrome logs a console warning that a preloaded resource wasn’t used within a few seconds.
width and height: reserve the space
Lazy images arrive late by design. If the page doesn’t know their size in advance, everything below jumps when they load, which is a Cumulative Layout Shift (CLS) problem. Setting width and height attributes fixes it, even for responsive images:
img {
max-width: 100%;
height: auto;
}Browsers use the attributes to compute an aspect ratio (here 400:300) before the image loads, and height: auto with that ratio reserves the right box at whatever width the layout gives it. The attributes don’t need to match the displayed size, only the ratio. The CSS aspect-ratio property does the same for elements without intrinsic dimensions.
Putting it together on the listing page
- The hero
<img>stays eager, getsfetchpriority="high", asrcsetwithsizes, andwidth/height. - The first row of product cards (four on desktop, two on mobile) stays eager at default priority. Lazy loading them would risk LCP on viewports where they’re the largest element.
- Every product image after the first row gets
loading="lazy"andwidth/height. - No preload is needed, because the hero is a real
<img>in the HTML. decoding="async"can be added to the lazy images; it lets the browser decode them off the main thread, though modern browsers already do this for most images.
Measure the result in Chrome DevTools’ Performance panel (the LCP marker and the network track show when the hero request starts) or with WebPageTest’s waterfall on a throttled mobile profile. Look for the hero request starting in the first group of requests, not after the scripts.
Interview practice
A news site’s article template sets loading="lazy" on every <img> to improve performance. Its LCP on mobile worsened from 2.1s to 2.9s after the change. The lead image is an <img> near the top. What happened, and what would you change?
Answer
The lead image is very likely the LCP element, and lazy loading removed it from the preload scanner’s early fetches. The browser now waits for CSS and layout to determine that it’s in the viewport before requesting it, so the request starts much later and LCP moves with it. The fix is to remove loading="lazy" from the lead image (or any image that can appear above the fold) and add fetchpriority="high" to it, keeping lazy loading for images further down the article. Make sure every image has width and height so lazy ones don’t cause layout shifts. Then confirm with a lab trace that the lead image’s request starts early, and watch the field LCP data (from the Chrome UX Report or your RUM) over the following weeks, since lab improvements don’t always match real users’ devices.
Keep going
- HTML and CSS topic hub
- HTML and CSS multiple-choice questions
- HTML responsive images and source selection
- CSS aspect-ratio and layout stability
- MDN: the fetchpriority attribute