Images are the heaviest thing on most content pages and the most common LCP offender. next/image automates responsive sizing, modern formats, lazy loading, and CDN delivery. Out of the box it covers most cases — but the defaults still trip people up on remote images, LCP priority, and host configuration.
Background
Next.js 15 ships next/image with sharp-based optimization, automatic AVIF/WebP, blur placeholders, and built-in lazy loading. On Vercel the optimization runs at the edge. On other hosts you either run a Node server, configure a loader, or pre-build optimized assets. Get the host story right first.
How to tell
- PageSpeed Insights flags “Properly size images” or “Serve images in next-gen formats”.
- Your LCP element is a hero image and LCP is over 2.5s on mobile.
next/imagethrows “Invalid src prop, hostname is not configured” on a deploy.- You see Cumulative Layout Shift caused by images loading after text.
Step by step
- Replace
<img>withnext/image. Static-imported images get free width/height inference and a built-in blur placeholder:
import Image from 'next/image';
import heroImg from '@/public/hero.png';
export default function Page() {
return (
<Image
src={heroImg}
alt="Dashboard showing weekly metrics"
placeholder="blur"
sizes="(max-width: 768px) 100vw, 800px"
priority // LCP image only — see step 2
/>
);
}
-
LCP image gets
priority. This opts out of lazy loading and addsfetchpriority="high". Only one or two images per page should be marked — typically the hero, never a footer logo. -
Remote images need
remotePatterns. Be specific, not**:
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
imageSizes: [16, 32, 64, 96, 128, 256, 384],
minimumCacheTTL: 31536000, // 1 year — assets are content-hashed
remotePatterns: [
{ protocol: 'https', hostname: 'images.unsplash.com', pathname: '/photos/**' },
{ protocol: 'https', hostname: 'cdn.yourdomain.com', pathname: '/**' },
],
},
};
export default nextConfig;
sizesmatters. Without it, the browser fetches the largest image indeviceSizesregardless of layout. Sample for a typical article hero (full width on mobile, ~800px on desktop):
<Image
src={heroImg}
alt="..."
sizes="(max-width: 640px) 100vw,
(max-width: 1024px) 80vw,
800px"
priority
/>
fillneeds a positioned parent. Otherwise the image collapses to zero height. Required for unknown aspect ratios:
<div style={{ position: 'relative', aspectRatio: '16/9' }}>
<Image src="/cover.jpg" alt="..." fill style={{ objectFit: 'cover' }} sizes="100vw" />
</div>
- Non-Vercel host? Pick a loader strategy. Static export forces a custom loader or
unoptimized:
// Cloudflare Images / Imgix / custom loader example
const nextConfig = {
images: {
loader: 'custom',
loaderFile: './lib/image-loader.ts',
},
};
// lib/image-loader.ts
export default function loader({ src, width, quality }) {
return `https://imgproxy.yourdomain.com/${width}/q${quality ?? 75}${src}`;
}
Or for a fully static export, give up optimization and gain portability:
const nextConfig = {
output: 'export',
images: { unoptimized: true },
};
- Verify with DevTools Network tab. The hero should request once with
image/avifcontent-type andfetchpriority: high:
# Check from terminal too
curl -sI 'https://yourdomain.com/_next/image?url=/hero.png&w=1080&q=75' \
| grep -iE 'content-type|cache-control'
# content-type: image/avif
# cache-control: public, max-age=31536000, immutable
- Measure LCP regression. Lighthouse points at the actual largest paint element:
npx lighthouse https://yourdomain.com/articles/foo/ \
--only-categories=performance --chrome-flags=--headless --quiet \
| grep -A1 'largest-contentful-paint\|LCP element'
Common pitfalls
- Forgetting
priorityon the LCP image — lazy loading delays the most important fetch, tanking LCP. - Whitelisting
**inimages.remotePatterns— every random hostname becomes an optimization target, increasing attack surface and costs. - Using
fillwithout a positioned parent (relative,absolute, orfixed) — image collapses to zero height. - Not setting
sizesand shipping a 2000px-wide image to a 400px-wide phone column. - Hosting on a non-Vercel platform without a loader configured, then wondering why optimization “does not work”.
- Disabling optimization (
unoptimized: true) site-wide as a fix for one bad image — you lose the whole point ofnext/image.
Who this is for
Any Next.js site shipping images — blogs, marketing pages, product galleries, doc sites with diagrams.
When to skip this
Pure text sites with zero images (rare). Still configure next/image defaults in case you add some later.
FAQ
- Does
next/imagework outside Vercel?: Yes — with a Node server it optimizes at request time, or you can use a custom loader (Cloudflare, Imgix). Static export needs an external loader. - Is AVIF worth it over WebP?: Yes for photographic content — AVIF is 20-30% smaller at similar quality.
next/imageserves AVIF when the browser supports it. - Should I use blur placeholders?: Yes for above-the-fold images — they reduce perceived load time. Static imports get them automatically.
- What about SVGs?: Inline them or use
<img>.next/imagedoes not optimize SVG and disables it by default for security.
Related
- Next.js Content-Site SEO: The Things That Bite
- Static or SSR: How to Pick for a Content Site
- Deploying a Next.js Site to Vercel
- Sitemap and robots.txt Basics in Next.js
Tags: #Indie dev #Next.js #Core Web Vitals #Technical SEO #Getting started