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.

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.jpginstead 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:
- Resize to a set of widths (e.g., 480, 768, 1200, 1920).
- Generate AVIF and WebP alongside a JPEG fallback.
- 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.