How Images Slow Down Your Site (And How to Fix It)
October 1, 2026 · 6 min read
Images are the biggest lever you have on page speed, and it is not close.
According to the 2025 Web Almanac, images are the Largest Contentful Paint element on roughly 85 percent of desktop pages and 76 percent of mobile pages, and account for around 48 percent of the weight of a median page. Whatever else is slow on your site, the image is usually the thing the metric is actually measuring.
The advice that follows from this is normally a checklist: compress, lazy load, use WebP. That is not wrong, but applied uniformly it makes things worse, because the most important image on your page needs the opposite treatment from all the others.
First, find out which image is the LCP element
Do not guess. Run a Lighthouse audit and look at the "Largest Contentful Paint element" section, which names the exact element.
People are frequently wrong about this. It is often not the hero image — it can be a logo, a thumbnail, or a block of text. Optimising the wrong image is effort that moves nothing.
Once you know, your images split into two groups that get treated differently.
The LCP image
This one is the metric. Everything you do to it should be about getting it on screen sooner.
Never lazy-load it. loading="lazy" on your LCP image directly delays the
number you are trying to improve. This is the most common self-inflicted wound in
image optimisation, and it usually comes from applying lazy loading site-wide
with a plugin or a global default.
Give it priority. fetchpriority="high" tells the browser this one matters.
For a hero that the browser discovers late — inside a carousel, or set via CSS
— a <link rel="preload"> in the head helps it start earlier.
But only one. Marking everything high priority is identical to marking nothing high. The whole mechanism is about relative ordering.
Make it small. This is the image where compression and correct dimensions pay the most, because every kilobyte delays the metric directly.
Every other image
The rest of the page gets the standard treatment.
loading="lazy" on anything below the fold, so the browser does not spend
bandwidth on images nobody has scrolled to.
decoding="async" so image decoding does not block other work. On
image-heavy pages, decode cost is a real contributor to sluggish interaction,
which shows up in INP.
Fewer of them. The cheapest image is the one you did not include.
Size them to what is actually displayed
This is the single largest saving available on most sites and the most commonly skipped.
A 1600 pixel hero looks fine on a desktop monitor. On a 375 pixel phone the browser downloads all of it and then shrinks it to fit, so most of those bytes were wasted. Serving images at the size they are actually displayed routinely cuts image weight by half or more with no visible difference.
Two parts to getting this right:
Resize your source images to the largest size they will ever be shown, rather than uploading whatever came off the camera. The resizer handles a batch at once.
Serve variants per screen with srcset and sizes:
<img
src="/photo-800.jpg"
srcset="/photo-400.jpg 400w, /photo-800.jpg 800w, /photo-1200.jpg 1200w"
sizes="(max-width: 600px) 100vw, 800px"
width="1200" height="800"
alt="">
Three widths is a reasonable baseline for content images; add a larger variant
for full-bleed heroes. The sizes attribute tells the browser how wide the image
will actually be, which it needs before layout in order to pick correctly.
Stop the layout jumping
CLS is the easiest of the three metrics to fix and images cause most of it.
Without dimensions, the browser does not know how much space to reserve, so the page reflows when each image arrives and everything below it jumps down. That is the shift the metric is measuring.
Always set width and height attributes on <img>. Modern browsers use them
to compute an aspect ratio and reserve the space, even when CSS resizes the image
responsively afterwards. Setting aspect-ratio in CSS works too.
This costs nothing and it is frequently the difference between passing and failing CLS.
Format, in one decision
AVIF compresses best. Encoding is slower and support outside browsers is thinner, but for images served from a build pipeline or a CDN that is not your problem.
WebP is typically 25 to 35 percent smaller than JPEG at matched quality and is supported everywhere that matters. If you pick one thing, pick this.
JPEG as the fallback, and for anything that leaves your website.
PNG only for graphics and transparency — never for photographs.
SVG for logos, icons and anything geometric. It scales to any size and is usually tiny.
Serve several with a <picture> element and let the browser choose:
<picture>
<source srcset="/hero.avif" type="image/avif">
<source srcset="/hero.webp" type="image/webp">
<img src="/hero.jpg" width="1200" height="630" alt="">
</picture>
Compress deliberately
Quality 80 is a good default for photographs and is invisible at normal viewing sizes. Below about 60, artefacts start appearing in smooth areas first — skies and skin band before detailed areas do.
Do this with the compressor and actually look at the result at full size, rather than trusting a number. Screenshots and images containing text need different treatment from photographs; one preset across a whole site is how you end up with a blurry logo.
Resize before you compress. Reducing dimensions removes pixels; compression only degrades the ones you keep. Aggressive compression at full resolution and gentle compression at correct dimensions can produce the same file size, and the second one looks far better.
Cache them properly
Images rarely change, so serve them with a long cache lifetime —
Cache-Control: public, max-age=31536000, immutable — and use fingerprinted
filenames so a new version gets a new URL. Repeat visits then cost nothing and
you never serve a stale file.
The order to work in
- Identify the LCP element with Lighthouse
- Make sure it is not lazy-loaded, and give it
fetchpriority="high" - Add
widthandheightto every image on the page - Resize source images to their maximum displayed size
- Convert to WebP, with AVIF if your pipeline supports it
- Compress, checking the result rather than trusting a preset
- Lazy-load everything below the fold
- Set long cache headers with fingerprinted filenames
Steps one to four are where most of the gain is, and none of them require a CDN, a plugin or a subscription.
The short version
Images are the LCP element on most pages, so they are usually the whole story. Find the one that is the LCP element and treat it as the exception — no lazy loading, high priority, aggressively optimised. Give everything else lazy loading and async decoding. Put width and height on all of them so nothing jumps. And before reaching for a better format, check you are not sending a 1600 pixel image to a 375 pixel screen, because that is usually the largest saving on the table.