Images are the biggest performance problem on most websites. A single unoptimized hero image can add 2MB to your initial load and drop your Lighthouse score by 30 points. Next.js has built-in image optimization via next/image, and it's gotten better with each release. Here's where things stand in Next.js 16 and what actually matters.
What next/image does for you
The <Image> component from next/image handles four things automatically:
- Format conversion. Serves WebP or AVIF instead of PNG/JPEG, based on the browser's Accept header. A 500KB JPEG becomes a 120KB WebP — same visual quality.
- Responsive sizing. Generates multiple sizes and serves the right one based on the viewport. A mobile user doesn't download a 2400px-wide hero image.
- Lazy loading. Images below the fold load only when the user scrolls near them. Built-in, no IntersectionObserver setup needed.
- Dimension enforcement. Requires
widthandheight(orfill), which prevents layout shift — the browser reserves space before the image loads.
This is the baseline. Use <Image> for every image unless you have a specific reason not to.
The priority prop: use it exactly once
The priority prop disables lazy loading and preloads the image. It should be on your largest above-the-fold image — usually the hero image or hero background.
<Image src="/hero.jpg" alt="Hero" width={1200} height={630} priority />
Only one image should have priority. I've seen codebases where every image in the first section has priority. The result: the browser tries to preload 6 images simultaneously, competing for bandwidth, and none of them load fast. This is what caused a 20-point Lighthouse drop on one of my projects — a below-fold image with priority that preloaded before the hero.
Static imports vs string paths
Two ways to reference images:
// Static import — optimized at build time
import heroImage from '@/public/hero.jpg'
<Image src={heroImage} alt="Hero" />
// String path — optimized at request time
<Image src="/hero.jpg" alt="Hero" width={1200} height={630} />
Static imports are better when possible. You get automatic width/height inference, blur placeholders, and build-time optimization. String paths require explicit dimensions and optimize on-demand.
For CMS images or user-uploaded content, string paths (or remote URLs with remotePatterns) are your only option. For everything in your repo, use static imports.
The sizes prop matters more than you think
Without sizes, Next.js generates images at default breakpoints and the browser picks one based on the full viewport width. This is often wrong.
If your image is in a 3-column grid, it's never wider than ~33% of the viewport. But without sizes, the browser assumes it might be 100% wide and downloads a larger image than needed.
// In a 3-column grid
<Image
src={project.image}
alt={project.title}
width={400}
height={300}
sizes="(max-width: 768px) 100vw, 33vw"
/>
This tells the browser: "On mobile, this image is full-width. On desktop, it's one-third." The browser downloads the appropriate size. On a real project, this reduced total image transfer by 40% without any visual change.
AVIF vs WebP
Next.js can serve AVIF, which is 20–30% smaller than WebP at the same quality. But AVIF encoding is slow — 5–10× slower than WebP. For build-time optimization (static imports), this adds significant time to your build.
The tradeoff:
- Few images (portfolio, landing page): enable AVIF. The build time hit is small, the size savings compound for returning visitors.
- Many images (blog with screenshots, e-commerce): stick with WebP. Build times matter, and the 20% savings per image doesn't justify 5× slower builds.
Configure in next.config.js:
module.exports = {
images: {
formats: ['image/avif', 'image/webp'], // AVIF first, WebP fallback
},
}
Blur placeholders
Blur placeholders show a low-resolution version of the image while it loads, preventing the "empty box then sudden image" flash. Static imports get this for free:
import heroImage from '@/public/hero.jpg'
;<Image src={heroImage} alt="Hero" placeholder="blur" />
For dynamic images, you need to generate the blur data yourself. The plaiceholder library does this at build time:
import { getPlaiceholder } from 'plaiceholder'
const { base64 } = await getPlaiceholder(imageBuffer)
<Image src={url} alt="..." placeholder="blur" blurDataURL={base64} />
Worth the effort for hero images and above-fold content. Not worth it for thumbnail grids where a gray placeholder is fine.
When to skip next/image
- SVG icons and logos.
next/imageprocesses SVGs through the optimizer unnecessarily. Use a regular<img>or inline the SVG. - Tiny images (<5KB). The optimization overhead exceeds the savings. Inline them as base64 or use a regular
<img>. - Background images via CSS.
next/imagerequires a DOM element. For CSSbackground-imagepatterns, optimize manually and serve from/public. - OG images. These are served to crawlers, not users. Generate them separately with
next/og.
The checklist
For every image on a page:
- Is it using
<Image>fromnext/image? If not, does it have a good reason? - Does exactly one above-fold image have
priority? - Does it have a
sizesprop that reflects its actual display size? - Is the source image reasonably sized? (Don't feed a 6000px photo into
next/image— resize to 2× the maximum display size first) - Is
alttext descriptive and unique?
Five questions. Run them on any page and you'll catch 90% of image performance issues.
Want your Next.js site to score 90+ on Lighthouse? Let's talk — image optimization is part of the performance baseline I ship on every project.