next/image 通过 /_next/image 路由按需转换:resize 加格式协商(AVIF/WebP)再加 CDN 缓存,首次请求耗算力,之后走缓存。
- 必须在
next.config.ts 中用 remotePatterns 白名单外部域名,旧的 images.domains 已弃用;未配置会抛 hostname is not configured。
- LCP 图片必须显式加
priority(Next.js 16 起使用 preload)。它会插入 <link rel="preload"> 并设置 fetchpriority="high"。
sizes 是响应式图片的关键:漏掉这一项时浏览器会按最大 deviceSizes 下载,移动端会白白拉大图。
- 本地静态导入的
src 会自动生成 blurDataURL;远端图片必须自己提供,建议用 Plaiceholder 之类的构建期工具生成 ≤ 100 字节的 base64。
- Vercel Image Optimization 按 源图 计费(Source Images),自建时用 Sharp 即可;关闭平台优化改走 Cloudinary/imgix 的
loader 也是常见方案。
next/image 到底做了什么
说实话,许多人第一次用 next/image 时以为它只是把 <img> 替换成一个「更聪明的」组件。事实上它背后是一条完整的请求期图像优化管线。当浏览器请求一张 next/image 输出的图片时,实际命中的是 Next.js 内置的 /_next/image 端点,这个端点会做四件事:
- 根据 URL query 里的
w(宽度)参数把原图 resize 到目标像素。目标宽度来自 deviceSizes / imageSizes 生成的候选集。
- 读取浏览器发来的
Accept 头,如果包含 image/avif 就返回 AVIF,其次是 WebP,都不支持才回退到原格式。
- 用 Sharp(libvips 绑定)压缩,quality 默认 75。
- 把结果写入磁盘(自建)或边缘节点缓存(Vercel),后续同参数请求直接命中缓存。
换句话说,首次请求某个尺寸变体的用户要为一次转换付费:CPU 时间、Sharp 依赖,可能还有冷启动。之后所有人都走 CDN 缓存。理解这一点后你会发现两件事。其一,源图越大越糟,4000 px 的 PNG 扔进 public/ 不是「零成本」,只是把成本推迟到第一次访问。其二,deviceSizes 数组每多一个断点,都会为每张图多引入一次冷缓存的可能性。
Next.js 15.4 起,next/image 默认已启用 image/avif 优先级,同时保留 image/webp fallback。Next.js 16 主要变化是把 priority 属性重命名为 preload,语义没变,但更贴合它做的事(插入 <link rel="preload">)。
最小可用示例与 width/height 的规则
最简单的用法是导入一张本地图片。因为是静态导入,Next.js 在构建期就能读到宽高和 blurDataURL,所以不用手写:
// app/page.tsx
import Image from 'next/image';
import hero from '@/public/hero.jpg';
export default function Page() {
return (
<Image
src={hero}
alt="产品概览"
priority {/* Next.js 16 请改成 preload */}
placeholder="blur"
sizes="100vw"
/>
);
}
如果 src 是字符串(远端图片或 /public 下的路径),你必须同时提供 width 和 height,或使用 fill。这是为了让 Next.js 在图片加载前就能在布局里预留正确的宽高比,从而避免 CLS(累积布局偏移)。
// 远端图片必须显式指定宽高
<Image
src="https://images.example.com/banner.jpg"
alt="活动 banner"
width={1600}
height={800}
sizes="(max-width: 768px) 100vw, 1600px"
/>
// 或者用 fill 让图片撑满父容器(父容器必须 position: relative 且有明确尺寸)
<div className="relative aspect-[2/1] w-full">
<Image src="/gallery/1.jpg" alt="" fill sizes="100vw" />
</div>
配置 remotePatterns:外部图片的正确姿势
如果你从 Cloudinary、S3、Sanity、Contentful 之类的服务加载图片,必须在 next.config.ts 里显式白名单,否则会直接抛出 hostname "xxx" is not configured under images in your next.config.js。
官方推荐的写法是 remotePatterns。旧的 images.domains 数组从 Next.js 15.3 开始被标记 deprecated,Next.js 16 会移除。remotePatterns 允许在 protocol、hostname、port、pathname、search 五个维度做精确匹配,安全性显著更高。毕竟 /_next/image 本质上是一个转发代理,白名单太宽等于把带宽和 CPU 免费送给互联网。
// next.config.ts
import type { NextConfig } from 'next';
const config: NextConfig = {
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
minimumCacheTTL: 60 * 60 * 24 * 30, // 30 天
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
pathname: '/**',
},
{
protocol: 'https',
hostname: '**.cloudfront.net',
pathname: '/uploads/**', // 只允许 uploads 目录,避免代理任意路径
},
{
protocol: 'https',
hostname: 'cdn.sanity.io',
pathname: '/images/PROJECT_ID/**',
},
],
},
};
export default config;
默认情况下 formats: ['image/webp'],只输出 WebP。手动加上 AVIF 后,Next.js 会按顺序尝试:「浏览器支持 AVIF?给 AVIF;否则 WebP;否则原图」。Can I Use 的 2026 年数据显示 AVIF 在全球移动端的支持率已超过 96%,主流场景下几乎可以按开就开。
| 维度 | WebP | AVIF |
| 相同视觉质量下体积(vs JPEG) | 小 25–34% | 小 40–55% |
| 编码 CPU 成本(相对) | 1× | 4–8× |
| 2026 浏览器覆盖率 | > 98% | > 96% |
| 动画支持 | 支持 | 支持 |
| 推荐场景 | 自建、CPU 受限 | Vercel、静态站 |
如果你部署在 Vercel,Image Optimization 是托管服务,AVIF 的额外编码成本对你不可见,直接开就行。如果自建(Node/Docker),流量峰值时 AVIF 转换可能把 CPU 打满,可以只用 WebP,或者引入独立的转换 worker 池。
如何用 priority/preload 优化 LCP
LCP(Largest Contentful Paint)是 Core Web Vitals 里对 SEO 影响最直接的指标,而首屏 hero 图往往就是 LCP 元素。next/image 默认是 lazy 的:它会等到图片进入视口才发起请求,这在下方内容里节省流量,但对首屏图是灾难。浏览器要先解析 HTML、构建 CSSOM、执行部分 JS,然后才知道要下载 hero。
给首屏图加 priority(Next.js 16 起改名为 preload)会做两件事:
- 在
<head> 里插入 <link rel="preload" as="image">,让浏览器在解析 HTML 时就并行下载。
- 给
<img> 加上 fetchpriority="high",提高在同一批下载中的优先级。
// app/(marketing)/page.tsx
export default function Home() {
return (
<section>
<Image
src={hero}
alt="首页 hero"
priority {/* Next.js 15 */}
// preload {/* Next.js 16 */}
sizes="100vw"
className="w-full h-auto"
/>
{/* 下方的图不要加 priority,让它们保持 lazy */}
<Image src={feature1} alt="特性 1" sizes="(max-width: 768px) 100vw, 33vw" />
</section>
);
}
一个来自 DebugBear 的真实案例把一张配错域名的 hero 图修好、加上 priority 后,LCP 从 4.1 s 降到 2.3 s,单一变更打进了 Google 的 "Good" 区间。我自己在一个 SaaS 落地页上也复现过类似结果,不过反过来的坑同样常见:给整页所有图片都加 priority,浏览器提示 "Image was detected as the Largest Contentful Paint (LCP). Please add the 'priority' property",你以为多多益善,结果 4 张图抢带宽反而拉长 LCP。规则:每个视口只有一张 LCP,理论上只需要一个 priority。如果不同断点下 LCP 元素不同(例如移动端换成 banner),可以标多张,其余保持 lazy。
sizes 与 srcset:不要让手机下载 2048 宽图
这一节是本文最容易被跳过、也最能立竿见影拉高 Lighthouse 分数的部分。next/image 会根据 deviceSizes 生成一套 srcset,例如 640w, 750w, 828w, 1080w, 1200w, 1920w, 2048w。但浏览器怎么知道该挑哪一档?答案是 sizes 属性。你必须告诉浏览器这张图在不同断点下会以多宽渲染。
没有 sizes 时浏览器会假设图片占满视口,直接下最大的那档(通常是 2048w)。对一张只在移动端渲染 375 px、桌面渲染 400 px 的头像来说,这意味着白白下载了近 25× 的像素。
// 三列网格里的卡片图
<Image
src={cover}
alt=""
width={800}
height={450}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
写 sizes 的思路是「由小到大」:从最窄断点开始,一路描述到最宽断点。浏览器会取第一个匹配的媒体查询。上面的例子表示:视口 ≤ 640 px 时图渲染满宽;≤ 1024 px 时占一半;再宽就占三分之一。浏览器结合 devicePixelRatio 挑最合适的 srcset 档位。
如果你在做一个和 Partial Prerendering 结合的营销页,绝大多数图片是 static shell 的一部分,正确的 sizes 能直接降低静态 CDN 上冷缓存的种类,也降低 Turbopack 生产构建后首屏抓取的字节数。
placeholder="blur" 与 blurDataURL 生成策略
placeholder="blur" 会在图片解码前显示一张模糊占位图,视觉上把「空到图」的突兀切换换成「模糊到清晰」的渐现,能显著改善感知性能。它默认是 empty,改成 blur 时必须搭配 blurDataURL,一个极小的 base64 数据 URL。
本地静态导入时 Next.js 会在构建期用 Sharp 生成一张 8 px 宽的极模糊 PNG,塞进产物里,你什么都不用做。远端图片就得自己提供:
// 方案 A: 构建期用 plaiceholder 生成
// npm i plaiceholder sharp
import { getPlaiceholder } from 'plaiceholder';
async function getBlur(src: string) {
const buffer = await fetch(src).then(r => r.arrayBuffer());
const { base64 } = await getPlaiceholder(Buffer.from(buffer));
return base64; // "data:image/jpeg;base64,/9j/4AAQSk..."
}
// 在 Server Component 里
export default async function ProductCard({ product }: { product: Product }) {
const blurDataURL = await getBlur(product.imageUrl);
return (
<Image
src={product.imageUrl}
alt={product.name}
width={600}
height={400}
placeholder="blur"
blurDataURL={blurDataURL}
/>
);
}
另一个反直觉的建议:不要给 LCP hero 用 blur 占位。你希望首屏图片直接清晰渲染,而不是先模糊一下。模糊帧会推迟 LCP 的判定时间。priority hero 图跳过 placeholder,其他图开 placeholder="blur",这是主流最佳实践。
外部 CDN loader:Cloudinary、imgix、自建
Next.js 允许你完全绕开内置的 /_next/image,改用外部 CDN 做转换。适用场景有三种:一是你已经买了 Cloudinary / imgix;二是自建 Node 下想避开 Sharp 依赖;三是走 Cloudflare Images 之类按次计价的服务。
// next.config.ts:全局 loader
const config: NextConfig = {
images: {
loader: 'custom',
loaderFile: './lib/cloudflare-loader.ts',
},
};
export default config;
// lib/cloudflare-loader.ts
import type { ImageLoaderProps } from 'next/image';
const CF_ACCOUNT = 'https://imagedelivery.net/YOUR_ACCOUNT_HASH';
export default function cloudflareLoader({ src, width, quality }: ImageLoaderProps) {
const params = [`width=${width}`, `quality=${quality ?? 75}`, 'format=auto'];
// src 只需要是 Cloudflare Images 的 image ID
return `${CF_ACCOUNT}/${src}/${params.join(',')}`;
}
切成外部 loader 后,remotePatterns 不再对该图生效(因为请求根本不过 /_next/image),你需要在 CDN 侧自己配安全策略。同时也要注意:外部 loader 意味着 next.config.ts 里的 formats、deviceSizes、quality 都被绕过,所有决策由 URL 参数控制。
缓存策略、Vercel 计费与自建成本
minimumCacheTTL 控制转换结果在服务端(或边缘)的最小缓存时长,默认 60 秒。把它调大是几乎无副作用的成本优化:只要源图 URL 稳定,30 天甚至 1 年都合理。缓存 key 由源 URL、w、q、Accept 组成,源图变更请改文件名(或 URL query)来 bust。
Vercel 的 Image Optimization 从 2025 年起改为按 Source Images 计费,每 1000 张唯一源图 5 美元起(Hobby 免费 1000 张)。这里的坑是「唯一源图」的定义包含所有 src 变体:如果你的 CMS 每次编辑重新签名 URL,或者你把 ?v=timestamp 拼进去做 cache-bust,唯一数会爆炸。合理做法是版本号写进路径(/images/v3/hero.jpg),而不是 query。我之前有个客户站点,因为签名 URL 每 15 分钟轮换,一个月账单被推到四位数,改成路径版本号后立刻回落。
自建部署(Node、Docker、Kubernetes)时 Sharp 是 next/image 的必需依赖。生产环境请显式 npm i sharp,Next.js 会 warn "Server did not have sharp installed",此时会退化到较慢的 Squoosh WASM 编码器。如果你的镜像是 node:20-alpine,Sharp 会拉预编译的 musl 二进制,无需 apk add。
常见报错与排错清单
没在 remotePatterns 里加白名单。检查 next.config.ts,重启开发服务器(next dev 修改 config 后必须重启)。
upstream image response failed for /path/to.jpg 403
源站拒绝了 /_next/image 的请求。常见原因是源站有 hotlink 保护或需要 signed URL,先在浏览器 curl -I 目标图片确认 HTTP 状态。
"Image was detected as the LCP element. Consider adding the 'priority' property"
控制台警告,含义是它检测到该图是 LCP 但没标 priority。加上就消失;如果你确认它不是 LCP(例如它只是 above-the-fold 但被 hero video 覆盖),忽略即可。
图片加载时布局抖动(CLS)
90% 是没写 width/height,或者用了 fill 但父容器没有明确尺寸。参考本文最小示例里的两种正确用法。想更系统地理解水合与首屏渲染的关系,可以看我们之前写的 Next.js Suspense 与 Streaming SSR 完全指南,LCP 优化和流式渲染是一对姊妹话题。
常见问题
next/image 和原生 img 有什么区别?
原生 <img> 只是把源文件按原样返回给浏览器;next/image 会在 /_next/image 端点上按请求参数做 resize、格式转换(AVIF/WebP)、srcset 生成、懒加载与缓存,配合正确的 sizes 与 priority 通常能节省 60–90% 的图片字节。
为什么 next/image 会报 hostname is not configured?
因为外部域名默认不允许被 /_next/image 代理。把域名加进 next.config.ts 的 images.remotePatterns(含 protocol、hostname、pathname),然后重启 next dev。
Next.js 16 里 priority 是不是被移除了?
没有移除,而是重命名为 preload。行为完全相同:插入 <link rel="preload" as="image"> 并给 <img> 加 fetchpriority="high"。priority 会作为别名保留一到两个版本,控制台给出 deprecation 提示。
如何为远端图片生成 blurDataURL?
常用方案是构建期用 plaiceholder 或 Sharp 自己 resize 到 8–16 px 后 base64 化,控制体积在 100 字节以内。运行时生成也可以,但要缓存结果,避免每次请求都跑一次 Sharp。
Vercel Image Optimization 的费用怎么算,怎么避免超支?
按「唯一源图」计费,同一 src 的所有尺寸变体只算一张源图。控制费用的核心是让 src URL 稳定:把版本号写进路径而不是 query、避免每次编辑重签 URL、必要时用外部 CDN loader 完全绕过 /_next/image。