Back to Next.js
Next.js
easy
mid

What does the Next.js Image component do that a plain img tag does not?

Next/Image gives you on-the-fly resizing, modern formats (AVIF/WebP), responsive srcset, lazy loading, and reserved layout space (no CLS). Plain <img> needs you to wire all of that yourself.

6 min read·~10 min to think through

Plain <img src="hero.jpg"> ships the source byte-for-byte to every device, blocks LCP if it's above-the-fold, and reflows the page when it loads (CLS). The Next.js <Image> component is a thin wrapper around <img> that adds the optimizations a production app would otherwise build by hand.

What <Image> does for you.

  1. On-the-fly resizing. Source is one large file; the runtime serves a per-device-width variant from /_next/image?url=...&w=...&q=.... Smaller bytes on mobile.
  2. Modern format negotiation. Returns AVIF or WebP when the browser sends Accept: image/avif,image/webp, falls back to the original format otherwise. Often 30–50% smaller than JPEG/PNG.
  3. Responsive srcset and sizes. Auto-generates multiple widths, lets the browser pick. You provide sizes describing the rendered width across breakpoints.
  4. Lazy loading by default. Off-screen images use loading="lazy" + IntersectionObserver. Above-the-fold images opt in with priority (which also adds <link rel="preload"> and disables lazy loading) — important for LCP.
  5. No layout shift. Requires either explicit width/height or fill + a sized parent. The component renders a placeholder of the correct aspect ratio so the page doesn't jump when the image arrives.
  6. Optional blur placeholder. placeholder="blur" shows a low-res preview while the full image loads — for static imports the blur data is generated at build time.
  7. Caching. Optimized images are cached on the edge (Vercel) or filesystem (self-hosted) with long-lived Cache-Control.

When plain <img> is fine. SVGs (already vector), tiny icons, images you've pre-optimized at build, third-party domains where you don't control caching, or when you genuinely want the simplest tag (e.g., user-uploaded avatars rendered through a CDN that already does the work).

Gotchas.

  • Remote images need a config allow-list. next.config.jsimages.remotePatterns (or domains legacy). Otherwise the runtime refuses to optimize.
  • fill requires a positioned parent. <div style="position: relative; aspect-ratio: 16/9"><Image src=… fill /></div>.
  • priority is for the LCP image only. Marking too many as priority defeats lazy loading.
  • Don't pair with unoptimized unless you really mean it (e.g., static export with no image server).
  • Cost. Self-hosted optimization runs on the Node server; high traffic can spike CPU. Vercel charges per optimized image. Pre-optimization at build time (with sharp) avoids this.

Core Web Vitals impact. Real-world wins: LCP drops by hundreds of ms because the image is smaller and preloaded; CLS drops to 0 because the slot is reserved; INP improves because lazy loading frees the main thread for interaction.

Code

tsx
import Image from "next/image";
import hero from "./hero.jpg"; // static import → known dimensions + blur

export function Hero() {
  return (
    <Image
      src={hero}
      alt="Customer dashboard"
      priority
      placeholder="blur"
      sizes="(min-width: 1024px) 50vw, 100vw"
      className="rounded-lg"
    />
  );
}
Above-the-fold hero — opt into priority
tsx
// next.config.js
// images: { remotePatterns: [{ protocol: "https", hostname: "cdn.example.com" }] }

<div className="relative aspect-video">
  <Image src="https://cdn.example.com/u/123.jpg" alt="" fill sizes="(min-width: 768px) 50vw, 100vw" />
</div>
Remote image with fill

Follow-up questions

  • Why does priority improve LCP and when should you NOT use it?
  • How does the optimizer pick between AVIF and WebP?
  • How would you self-host the image optimizer at scale?
  • When would you skip <Image> entirely?

Common mistakes

  • Using <img> for the LCP hero — ships full bytes, no preload, no srcset.
  • Forgetting width/height (or fill + sized parent) → CLS regression.
  • Adding priority to every image — defeats lazy loading and bloats the preload list.
  • Loading a remote CDN image without configuring remotePatterns — runtime refuses.

Performance considerations

  • Set realistic `sizes` — wrong values cause the browser to download a too-large variant.
  • Use static imports when possible: dimensions known at build, blur generated for free.
  • Cache headers on /_next/image are long-lived; bust by changing the source URL.

Edge cases

  • SVGs are not optimized — pass through as-is or use plain <img>.
  • Animated GIFs become static frames after optimization unless you set unoptimized.
  • Cross-origin srcset requires CORS headers if you also use crossOrigin.

Real-world examples

  • Vercel marketing pages, Linear's docs, Notion's blog — all rely on next/image for LCP.

Senior engineer discussion

Senior signal: connecting next/image features back to specific Web Vitals (LCP, CLS), knowing when NOT to use it, and understanding the cost model (Vercel-hosted vs self-hosted vs build-time).

Related questions