Your images make the page slow
The situation
The page works, but loading feels heavy — the hero takes seconds to appear, content jumps as images pop in, and the Network tab shows 8MB of transfer for a page with a dozen images. Lighthouse is yelling about LCP and “properly size images”.
Diagnosis: why it happens
Images are usually most of a page’s bytes, and the waste compounds from three independent mistakes:
- Wrong size. Shipping a 4000px-wide original into a 400px slot sends ~100× the pixels the screen can show (pixels scale with width squared).
- Wrong format. JPEG/PNG cost roughly 30–50% more bytes than WebP/AVIF at the same visual quality.
- Wrong timing. All images load eagerly, so below-the-fold images compete for bandwidth with the hero image the user is actually waiting for — directly hurting LCP. And images without declared dimensions cause layout shift (CLS) as each arrival re-runs layout (see Frontend fundamentals).
Multiply the three and an 8MB page was really a ~400KB page wearing the wrong clothes.
Recommendation: right format, right size, lazy below the fold
Before — one original for everyone, eager:
<img src="/photos/hero-original.jpg" class="hero"> <!-- 4.2MB, 4000px --><img src="/photos/product-1.jpg" class="card-thumb"> <!-- 1.8MB for a 300px slot -->After — modern format, responsive sizes, lazy where it should be:
<!-- Hero: the LCP element. Eager + high priority. --><img src="/photos/hero-1200.avif" srcset="/photos/hero-800.avif 800w, /photos/hero-1200.avif 1200w, /photos/hero-2000.avif 2000w" sizes="100vw" width="1200" height="600" fetchpriority="high" alt="…">
<!-- Below the fold: lazy. --><img src="/photos/product-1-300.avif" srcset="/photos/product-1-300.avif 300w, /photos/product-1-600.avif 600w" sizes="(max-width: 600px) 50vw, 300px" width="300" height="300" loading="lazy" decoding="async" alt="…">The rules, in priority order:
- Always
width/height(or CSSaspect-ratio) — reserves space, kills CLS. Free. - AVIF/WebP — build-time conversion (
sharp) or your framework’s image pipeline. srcset+sizes— the browser picks the right resolution for the device.loading="lazy"below the fold only — never on the LCP image; that adds delay. Give the herofetchpriority="high"instead.
Watch what width/height alone buys — same slow images, with and without reserved
space:
When does it matter? A text-heavy page with 3 small images already sub-100KB won’t feel different — check the Network tab before engineering. Where it always matters: image-led pages (commerce, galleries, landings), mobile networks, and any LCP > 2.5s where the LCP element is an image — that’s this case almost by definition.
Discarded alternatives (and when they ARE the right call)
An image CDN (Cloudinary, imgix, Cloudflare Images)
- What it solves: everything above via URL parameters (
?w=400&format=auto) at request time — no build pipeline, handles user-uploaded content, serves from edge. - Why it’s not the first option here: a paid dependency for what
<img srcset>+ build-time conversion do for free when your images are known at build time. - When it IS the right call: user-uploaded images (you can’t pre-generate what you haven’t seen) or catalogs of thousands where build-time processing gets slow.
The framework’s image component (next/image, Astro <Image>)
- What it solves: generates the srcset/formats/dimensions automatically — the whole
recommendation with less manual work and no forgotten
sizes. - Why it’s not the first option here: it isn’t discarded so much as deferred: use
it if you’re on such a framework. Knowing the manual mechanics still matters — a wrong
sizesprop silently ships oversized images through these components too. - When it IS the right call: any Next/Astro/Nuxt project — it’s the default there, not the alternative.
Blur-up placeholders / LQIP
- What it solves: perceived speed — a blurred preview appears instantly, so arrival feels smooth rather than abrupt.
- Why it’s not the first option here: pure polish; changes zero bytes and zero LCP. Adding it before fixing sizes is decorating the slow.
- When it IS the right call: after the fundamentals, on image-led UIs (galleries, feeds) where the aesthetic matters. Cheap with framework support.
Leaving it as is
- What it solves: no build pipeline, no variants to manage.
- Why it’s not the first option here: the case starts from measured slowness.
- When it IS the right call: internal tools on office networks, pages with trivial
image weight, prototypes. Even then,
width/heightattributes are free — do those.
How to measure before and after
- Lighthouse (DevTools → Lighthouse): before/after LCP and CLS; the “Properly size images” audit tells you exactly which images and how many wasted KB.
- Network tab, filtered to Img, disable cache + throttle to Fast 4G: total image bytes and whether below-the-fold images load at all before you scroll (they shouldn’t).
- The LCP element itself: Performance panel → LCP marker — confirm it’s the hero image and watch its timestamp drop.
Signals you need something else
- Images optimized but LCP still bad → the bottleneck is the server or render-blocking resources → Your endpoint is slow if it’s TTFB; check fundamentals for the rest.
- Hundreds of images in a scrolling list → the DOM is the problem too → Your huge list renders slowly.
- User-uploaded images at scale → image CDN (above), and validate/resize at upload time.
Related resources
- Frontend fundamentals: the render pipeline
- MDN — Responsive images:
srcset/sizesexplained precisely - web.dev — Optimize LCP: the full LCP playbook, images included
- web.dev — Browser-level lazy loading: how
loading="lazy"behaves, and why not on the hero