← Back to blog
[nextjs]September 7, 2026· 3 min read

Beyond <Image/>: Real‑World Image Prep for Next.js Performance

Learn why Next.js <Image/> isn’t a magic fix. Discover practical source‑image workflows, naming, formats, and build‑time tricks that keep your pages fast at scale.

#image#optimization#workflow#performance#nextjs

Why alone isn’t enough

Next.js does a great job serving the right size to the browser, but it assumes the source file is already sane. If you feed it a 6000px PNG named IMG_1234.png, the component will still have to download that giant file before it can shrink it on the fly. The cost is paid in bandwidth, cache‑misses, and LCP spikes.

Layer 1: Source preparation

Treat the image as a code artifact. Before it ever touches your repo or CMS, answer these questions:

  • Is the resolution excessive? A hero that never exceeds 1920 px on desktop should be capped at that width.
  • Is the crop intentional? Trim dead space in Photoshop, Figma, or a CLI tool.
  • Do you need transparency? If not, convert PNG → JPEG or AVIF to shave kilobytes.
  • Is the filename descriptive? Use hero‑product‑blue‑1920w.jpg instead of a random hash.
  • Do you need multiple variants? Create WebP/AVIF and a fallback JPEG once, not on every request.

These steps belong to your asset pipeline, not to Next.js at runtime.

Automating the pipeline with a build step

Most teams use sharp or imagemin in a script that runs on npm run build. The script can:

  1. Resize to a set of widths (e.g., 480, 768, 1200, 1920).
  2. Generate AVIF and WebP alongside a JPEG fallback.
  3. Rename files with a hash for cache busting.
const sharp = require('sharp');
const fs = require('fs');
const src = 'assets/raw/hero.png';
const outDir = 'public/images/hero';
const widths = [480, 768, 1200, 1920];
(async () => {
  const buffer = await sharp(src).removeAlpha().toFormat('jpeg').toBuffer();
  for (const w of widths) {
    await sharp(buffer)
      .resize(w)
      .toFile(`${outDir}/hero-${w}.jpeg`);
    await sharp(buffer)
      .resize(w)
      .toFormat('webp')
      .toFile(`${outDir}/hero-${w}.webp`);
  }
})();

After the script runs, your public/images/hero folder contains ready‑made assets that <Image/> can reference without extra work.

Layer 2: Integrating with

Now you tell Next.js which variant to use. The src can be a relative path to the smallest version; the component will request larger sizes via srcSet automatically.

import Image from 'next/image';

export default function Hero() {
  return (
    <Image
      src="/images/hero/hero-480.jpeg"
      alt="Blue product on a white background"
      width={1920}
      height={1080}
      priority
    />
  );
}

Because the 480 px file is already tiny, the initial download is cheap. The browser then upgrades to 768 px or 1200 px only if the viewport demands it.

Layer 3: Runtime sanity checks

If you pull images from a headless CMS, you still need a guardrail. A simple webhook that runs the same sharp script on every upload guarantees that no rogue 8 k PNG slips into production.

Combine that with a naming convention enforced in the CMS schema (e.g., type‑width.format) and you have a self‑healing pipeline.

Bottom line

Next.js <Image/> is a delivery layer, not a source‑image optimizer. The real performance win comes from treating images as first‑class assets: trim them, choose the right format, generate purposeful variants, and lock the process into your build or CMS workflow. When those steps are in place, <Image/> can finally live up to its hype without hiding a broken pipeline behind it.

// related