Skip to main content

Sep 2026 · Guide · ~10 min read

Next.js Image Optimization — Everything You Need to Know

By Safdar Ali — frontend engineer, Bengaluru

I'm Safdar Ali. Images are the number-one LCP killer on sites I audit in Bengaluru — hero photos shipped as 2MB PNGs, no sizes, lazy loading on above-the-fold content. Next.js image optimization via next/image fixed that on a client marketing rebuild where LCP went from 4.2s to 1.7s — full story in my 60% load time case study. This guide is everything I configure on every new nextjs image optimization pass.

The Image Optimization API runs on your deployment host — Vercel, self-hosted, or configured loader. It is not magic file compression in git; it transforms at request time based on device width and Accept headers for AVIF and WebP. That means first visitor to a new size variant pays a transformation cost; subsequent visitors hit CDN cache. Warm critical images after deploy by loading the homepage from Mumbai throttling in PageSpeed Insights or with a simple curl loop on hero URLs.

nextjs image optimization is not a substitute for good source assets. A 4000px PNG dropped into public/ still costs edge CPU to resize — you just pay later instead of upfront. I export heroes at 1920px wide WebP from Figma or Squoosh before commit. Thumbnails at 800px are enough for card grids. The optimizer scales down; it cannot fix blurry upscales or bloated originals uploaded by marketing teams who export at print resolution out of habit.

Why next/image beats a raw img tag

The Image component requests correctly sized WebP/AVIF variants, reserves layout space to prevent CLS, and can prioritize LCP candidates. A plain <img src="hero.png"> downloads the full file every time.

Width and height props reserve aspect ratio before the image loads — Core Web Vitals CLS drops when layout stops jumping. fill mode requires a positioned parent with defined dimensions; without it, Lighthouse reports layout shift and users on slow networks see content jump under their thumb. I treat missing sizes as a merge blocker on any PR that touches marketing pages, the same way I block missing alt text.

Decorative images should use empty alt — alt="" — so screen readers skip them. next/image still needs alt for accessibility audits; pair with my WCAG guide when icons sit beside photos. For purely decorative hero backgrounds, consider CSS background-image instead of Image when no meaningful alt text exists — but if the image conveys brand story, write alt text that describes the scene, not "hero image".

// BEFORE — raw img, layout shift, huge file
<img src="/hero.png" alt="Product launch" className="w-full" />

// AFTER — next/image
import Image from "next/image";

<Image
  src="/hero.png"
  alt="Product launch"
  width={1200}
  height={630}
  priority
  className="w-full h-auto"
  sizes="100vw"
/>

The sizes prop — the setting most teams get wrong

sizes tells the browser which responsive width to request. Wrong sizes means downloading a 1200px image for a 400px card — wasted bytes on Indian mobile networks.

The browser uses sizes to pick from srcset widths the optimizer generates. If sizes says 33vw on desktop but your CSS makes the image 50vw wide, the browser may download a smaller file than needed and look blurry on retina displays — or the opposite, wasting bytes. Match sizes to your Tailwind breakpoints: sm, md, lg, xl. I copy the same media queries from the grid className into sizes strings to keep them in sync.

quality prop defaults to 75 — lowering to 60 on thumbnail grids saves bytes with minimal visible loss on small cards. Do not lower quality on hero images; banding in gradients shows on project screenshots. Test on a real Android phone, not only Retina MacBook screens in a Bengaluru coworking space.

// Card grid: full width mobile, half tablet, third desktop
<Image
  src={project.thumbnail}
  alt={project.title}
  width={640}
  height={360}
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  className="rounded-xl object-cover"
/>

// Hero: always full viewport width
<Image
  src="/hero.webp"
  alt="Safdar Ali portfolio"
  fill
  priority
  sizes="100vw"
  className="object-cover"
/>

priority and LCP — one hero, not ten

LCP is usually the largest image or text block above the fold. Set priority on exactly one LCP image per page — typically the hero. Every other image should lazy-load by default.

// BEFORE — priority on logo, avatar, AND hero (bandwidth fight)
<Image src="/logo.svg" priority />
<Image src="/avatar.jpg" priority />
<Image src="/hero.webp" priority />

// AFTER — priority only on LCP candidate
<Image src="/logo.svg" alt="Logo" width={120} height={40} />
<Image src="/hero.webp" alt="Hero" fill priority sizes="100vw" />

Target LCP under 2.5s for "good" Core Web Vitals. On 4G in India, that requires hero under ~200KB delivered (AVIF helps).

fetchPriority is related but separate — on Next.js 15 you can set fetchPriority="high" on the LCP image alongside priority. Preload link tags in layout.tsx are a fallback when the hero is a background image outside next/image. Document which page owns the single LCP candidate in your performance checklist so designers do not add a second full-width banner without engineering review.

Remote images — CMS, Cloudinary, S3

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      { protocol: "https", hostname: "cdn.sanity.io", pathname: "/images/**" },
      { protocol: "https", hostname: "res.cloudinary.com", pathname: "/my-account/**" },
    ],
    formats: ["image/avif", "image/webp"],
  },
};

export default nextConfig;
// CMS-driven image
<Image
  src={post.coverUrl}
  alt={post.title}
  width={1200}
  height={675}
  sizes="(max-width: 768px) 100vw, 720px"
/>

unoptimized prop bypasses the optimizer for rare cases — GIF animation, SVG served as static when you should use inline SVG instead, or domains you cannot whitelist. Use sparingly; every unoptimized hero negates the LCP work elsewhere. Custom loaders integrate Cloudinary and Imgix if you already pay for transformations there — set loader in next.config and pass loader props on Image components.

fill layout and aspect ratio — avoid CLS

// Parent MUST have position relative + defined aspect
<div className="relative aspect-video w-full overflow-hidden rounded-xl">
  <Image src={src} alt={alt} fill className="object-cover" sizes="(max-width: 768px) 100vw, 50vw" />
</div>

// BEFORE — fill without aspect parent = layout jump
<div>
  <Image src={src} alt={alt} fill />
</div>

object-cover versus object-contain changes what users see inside aspect-ratio boxes — product shots often need contain to avoid cropping labels; hero photos need cover for full-bleed impact. Test on narrow phones common in India — 360px wide viewports expose bad crops before desktop QA does. sizes on fill images still matters; the browser picks srcset width from sizes even when width and height props are absent because fill mode uses the parent box.

Static imports — automatic width/height

import hero from "@/public/hero.webp";
import Image from "next/image";

export function Hero() {
  return (
    <Image
      src={hero}
      alt="Launch"
      placeholder="blur"
      priority
      sizes="100vw"
    />
  );
}

Static imports give blur placeholder for free — nice polish without extra requests on safdarali.in.

placeholder="blur" with static imports shows a low-quality preview while the full image loads — great on portfolio grids where users scroll quickly. Do not blur the LCP hero if it delays perceived load; priority hero should appear sharp on first paint. blurDataURL manual base64 strings work for remote images when you cannot static import — generate tiny placeholders with plaiceholder or similar build-time tools on content-heavy sites.

img vs next/image — quick comparison

CriteriaRaw imgnext/image
Responsive sizesManual srcsetAutomatic via sizes prop
Modern formats (AVIF/WebP)You convert assetsOn-demand at request
Layout shift (CLS)Common without width/heightReserved space by default
Lazy loadingloading="lazy" manualDefault below fold
LCP priorityfetchpriority manualpriority prop
Blur placeholderCustom LQIPStatic import blur
CDN / edge resizeSeparate image CDNBuilt into Next.js optimizer
Best for icons/SVGYesNo — use inline SVG

On marketing pages I audit in Bengaluru, switching hero and card images to next/image alone often cuts transferred image bytes by half. Pair that with correct sizes and one priority image — that is the combo that moved LCP under 2.5s in my 60% load time rebuild.

deviceSizes and imageSizes in next.config tune which widths the optimizer generates — default breakpoints work for most marketing sites. If your layout never shows images wider than 720px, tightening deviceSizes reduces transformation variants and storage on self-hosted optimizers. Document changes when designers add a new full-bleed breakpoint — sizes strings and config must move together or you regress LCP silently.

quality prop — balance sharpness and bytes

Default quality is 75 — good for most photos. Product shots and portfolio thumbnails can drop to 60–65 on mobile-heavy traffic without visible loss. Never set quality to 100 on full-width heroes unless you have a reason.

// Thumbnail grid — lower quality saves KB on 12 images per page
<Image
  src={project.thumbnail}
  alt={project.title}
  width={400}
  height={225}
  quality={65}
  sizes="(max-width: 768px) 50vw, 25vw"
/>

// Hero — default quality, priority, full width
<Image src="/hero.webp" alt="Launch" fill priority sizes="100vw" />

Measuring LCP and image bytes — before you ship

Chrome DevTools → Performance → LCP marker. PageSpeed Insights from Mumbai throttling (if available) or Lighthouse mobile. Compare transferred bytes before/after next/image — I expect 40–70% reduction on image-heavy pages.

Real Experience Metrics in Search Console lag days behind deploys — use field data for trends, lab data for debugging a specific PR. I screenshot Lighthouse before and after image PRs and attach to GitHub so clients see measurable delta. When LCP element is text not image, priority on images will not help — fix font loading with next/font instead.

// Checklist before launch
// [ ] Hero uses priority + sizes="100vw"
// [ ] Below-fold images: no priority
// [ ] WebP/AVIF source assets where possible
// [ ] remotePatterns configured for CMS
// [ ] No img tags left on marketing paths
// [ ] aspect-* or width/height on every Image

Broader perf: web performance checklist 2026.

// BEFORE — same 2400px file for every breakpoint
{projects.map((p) => (
  <img key={p.id} src={p.image} alt={p.title} className="w-full" />
))}

// AFTER — each card requests ~400px variant on mobile
{projects.map((p) => (
  <Image
    key={p.id}
    src={p.image}
    alt={p.title}
    width={400}
    height={225}
    sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
    className="rounded-xl object-cover"
  />
))}

Export source assets as WebP where possible before they hit the optimizer — garbage in still wastes CPU on the edge. I resize hero sources to 1920px max width in Figma or Squoosh before dropping them in public/ — the optimizer cannot invent detail from a 6000px camera dump.

Real Experience Metrics in Search Console lag days behind deploys — use field data for trends, lab data for debugging a specific PR. I screenshot Lighthouse before and after image PRs and attach to GitHub so clients see measurable delta. When LCP element is text not image, priority on images will not help — fix font loading with next/font instead.

When NOT to use next/image

Small SVG icons, inline data URIs, and animated GIFs where optimization strips behavior — use regular img or SVG components. Do not wrap 16px icons in the image optimizer.

Art direction sometimes needs different crops on mobile versus desktop — two Image components with responsive display classes can be simpler than one picture element. Document the pattern in your component library so marketing pages stay consistent across projects I ship from Bengaluru.

// Icons — inline SVG or lucide-react, not next/image
import { ArrowRight } from "lucide-react";
<ArrowRight className="h-4 w-4" aria-hidden />

The single takeaway

Next.js image optimization is not automatic magic. You still choose source quality, sizes, and priority. Do that right and LCP follows — the same lever that moved Lighthouse performance from 54 to 91 in my App Router case study.

Self-hosting the image optimizer on Railway or Docker is an option when Vercel bandwidth limits bite — set images.loader in next.config and point to your transformer. That is advanced; hobby Vercel handles portfolios fine. Measure before migrating — optimizer ops cost engineering time that might be better spent compressing source assets and fixing sizes props on twelve pages.

Audit every img tag in the repo with grep — stragglers hide in email templates ported to React, footer badges, and MDX blog content. nextjs image optimization only helps where you use next/image. One forgotten hero img on the homepage caps your Lighthouse performance score no matter how perfect the rest of the site is. I fix stragglers first in every perf engagement from Bengaluru before touching JavaScript bundles.

OG images for social sharing can use next/image in opengraph-image.tsx routes — same optimizer, correct 1200×630 dimensions. Twitter and LinkedIn compress again, but starting from a small AVIF source beats a multi-megabyte PNG in metadata. Keep the link to my performance case study in mind when stakeholders ask why the blog feels faster after an image-only PR.

decoding="async" is default on next/image for non-priority images — do not fight it. For above-fold non-LCP images like a secondary banner, leave default lazy behavior off only if art direction requires it; otherwise you compete with the hero for bandwidth on first load. nextjs image optimization is a system: config, component props, source assets, and discipline about how many bytes each route deserves on Indian mobile networks.

Related: RSC vs client components, projects, contact.

If this helped you

I publish free tutorials and write-ups like this in my spare time — no paywall on the guides. If it saved you an afternoon of trial and error, you can support the work:

More guides on safdarali.in — same author, production-focused.

"Talk is cheap. Show me the code."