PerformanceIntermediate~1 hour

Optimize images

Unoptimized images are the single biggest cause of slow page loads — a few oversized JPEGs can dwarf all your JS and CSS combined, tanking your LCP score, mobile data usage, and Google ranking. This recipe makes images load fast without hurting quality.

the approach

Don't hand-roll image optimization in 2026 — treat it as a solved, platform-level problem. Your job is two things: (1) pick a layer that does format negotiation, resizing, and caching for you — your framework's image component (Next.js `<Image>`) or an image CDN (Cloudflare Images, Cloudinary) — and (2) feed it the right hints: a correct `sizes` value for your layout, explicit width/height to kill layout shift, and `priority` on the one hero image that is your LCP. Everything else (AVIF/WebP negotiation, srcset generation, lazy-loading) should happen automatically. If you find yourself writing a `sharp` loop by hand, it's usually because you have no CDN — not because you should.

Step by step

  1. 1

    Measure first — find what's actually heavy

    Run Lighthouse (in Chrome DevTools) and open the Network tab filtered to Img. Note total image weight, which element is the LCP, and any image flagged under 'Properly size images' or 'Serve images in next-gen formats'. You optimize what you measure, not what you guess.

  2. 2

    Adopt a component or CDN that negotiates formats

    Use your framework's image primitive (Next.js `<Image>`, Astro `<Image>`) or an image CDN (Cloudflare Images, Cloudinary, imgix). These auto-serve AVIF or WebP based on the browser's Accept header, so you never manually export multiple formats.

  3. 3

    Serve responsive sizes with correct `sizes`

    A phone should never download a 2000px desktop image. The component generates a `srcset` automatically, but only if you give it an accurate `sizes` (e.g. `(max-width: 768px) 100vw, 50vw`). Wrong `sizes` is the #1 reason 'responsive' images still ship the full file.

  4. 4

    Set explicit dimensions to prevent layout shift

    Always provide width and height (static imports infer them; remote images need them passed) or a CSS aspect-ratio. This reserves space so content doesn't jump as images load — killing Cumulative Layout Shift (CLS).

  5. 5

    Prioritize the hero, lazy-load the rest

    Mark your above-the-fold LCP image with `priority` (Next.js) or `fetchpriority="high"` + `loading="eager"`. Everything below the fold should stay `loading="lazy"` (the default). Lazy-loading your hero is a common self-inflicted LCP wound.

  6. 6

    Tune compression, don't crush it

    Target quality ~75-80 for AVIF/WebP and strip EXIF metadata. Spot-check gradients, skies, and skin tones for banding or blotches — those artifact first. For a build-time pipeline without a CDN, `sharp` or Squoosh does this well.

  7. 7

    Handle user uploads at the edge, not just static assets

    If users upload images, pipe them through the same CDN with on-the-fly transforms (resize/format/quality via URL params) and make sure transformed variants are cached — otherwise every request re-transforms and your bill and TTFB balloon.

  8. 8

    Re-measure and set a budget

    Run Lighthouse again to confirm the format/sizing audits pass. Add a performance budget or an image-size check in CI so a future 4MB PNG fails the build instead of silently shipping.

Starter snippet

tsx
import Image from "next/image";
import hero from "@/public/hero.jpg"; // static import -> width/height + blur inferred

export function Hero() {
  return (
    <Image
      src={hero}
      alt="Team collaborating in a bright office"
      priority           // this is the LCP image: preload it, don't lazy-load
      placeholder="blur" // auto blur-up while the real image decodes
      sizes="(max-width: 768px) 100vw, 50vw" // drives the generated srcset
      className="rounded-xl object-cover"
    />
  );
}

// Next.js serves AVIF/WebP automatically based on the request's Accept header,
// generates a srcset from `sizes`, and reserves layout space from the import.
// Below the fold: drop `priority` — images lazy-load by default.

✕ Watch out for

  • Lazy-loading your hero/LCP image — it delays the one paint Google measures. Mark it `priority` / `fetchpriority="high"` instead.
  • Not setting width/height (or aspect-ratio), so images pop in and shove content around — that's Cumulative Layout Shift, and it's a ranking factor.
  • Shipping a 4000px original and shrinking it with CSS `width`. The browser still downloads the full file; you must serve a physically smaller image via srcset.
  • Over-compressing AVIF/WebP — quality below ~60 causes visible banding on gradients and blotchy skin tones. Always eyeball the result.
  • Transforming user uploads on every request with no caching, so your image CDN bill and TTFB explode under traffic.

✓ Pro tips

  • When prompting an AI agent, name your exact framework/CDN and tell it to use the native image component with a correct `sizes` for each layout and `priority` on the hero — then verify: view source and confirm a `srcset` exists, and check the Network tab shows the image served as AVIF or WebP. Agents routinely drop `sizes` (giving you a fake-responsive image) or forget `priority`.
  • Treat Lighthouse's 'Properly size images' and 'Serve images in next-gen formats' audits as pass/fail gates, before and after your change — don't trust that it 'looks fine'.
  • Keep your high-res originals as the source of truth and generate optimized variants from them; never re-compress an already-compressed file, which stacks artifacts.
  • Add a CI check that fails the build if any committed image exceeds a size budget, so regressions can't sneak back in.