⚡ AMP
Next.js

Next.js Image component and image optimization in production

A practical guide to next.js Image component and image optimization in production.

Nitheesh DR 3 min read

Next.js Image Component and Image Optimization in Production

Your Lighthouse score tanks on the "Largest Contentful Paint" metric because your hero image is a 4MB PNG served at its native 3000px width to a 400px-wide mobile viewport. This is one of the most common — and most fixable — performance problems on the web, and Next.js's Image component exists specifically to fix it without you hand-rolling responsive srcset logic.

The basic swap

// Before: no optimization, no lazy loading, no responsive sizing
<img src="/hero.png" alt="Product hero" />

// After: automatic resizing, format conversion, lazy loading
import Image from "next/image";

<Image
  src="/hero.png"
  alt="Product hero"
  width={1200}
  height={630}
  priority
/>

width/height aren't just for sizing — they let the browser reserve layout space before the image loads, preventing cumulative layout shift (a Core Web Vitals metric Google actually ranks on). priority disables lazy loading for above-the-fold images like a hero, since lazy-loading your most important image is counterproductive.

What's actually happening under the hood

Next.js runs images through an optimization API route (/_next/image) that resizes on demand, converts to modern formats (WebP or AVIF) when the browser supports them, and caches the result. You get this without configuring an image CDN yourself — though for high-traffic production sites, pairing it with a real CDN in front of Vercel or your host is still worth it.

Remote images need explicit permission

// next.config.ts
const nextConfig = {
  images: {
    remotePatterns: [
      { protocol: "https", hostname: "images.unsplash.com" },
      { protocol: "https", hostname: "cdn.yoursite.com" },
    ],
  },
};

This is a security boundary, not busywork — without it, Next.js refuses to optimize images from arbitrary external domains, since blindly fetching and processing any URL a request supplies is an SSRF risk.

Fill mode for unknown aspect ratios

When you don't know the image's dimensions ahead of time (user uploads, CMS content):

<div style={{ position: "relative", width: "100%", height: "400px" }}>
  <Image
    src={post.coverImage}
    alt={post.title}
    fill
    className="object-cover"
    sizes="(max-width: 768px) 100vw, 50vw"
  />
</div>

fill makes the image absolutely positioned to cover its parent, which needs position: relative and an explicit size. The sizes attribute tells the browser which image size to request at which viewport width — get this wrong and you'll either over-fetch (full-size image on mobile) or under-fetch (blurry image on desktop).

Common mistakes

What I'd actually use

next/image for everything except truly tiny decorative icons (where an inline SVG is simpler and avoids a network request entirely). Set priority on exactly one image per page — the actual LCP element — and nothing else. For a CMS-driven blog, generate blurDataURL placeholders at build/fetch time so images fade in smoothly instead of popping in, which meaningfully improves perceived performance even when raw load time is unchanged.

Next steps

Run Lighthouse against your current production build and look for "Properly size images" and "Serve images in next-gen formats" warnings — nearly every flagged image is fixed by swapping a bare <img> for next/image with correct sizes.