#Next.js#React

Web 图片优化:格式、响应式与懒加载

图片是页面最大的资源,优化图片 = 优化 LCP。本文讲清现代图片格式(WebP/AVIF vs JPEG/PNG)、响应式图片(srcset/sizes)、Next.js Image 组件、懒加载与占位、CDN 动态调整,给一套生产级图片优化方案。

本文要点

  • 主题聚焦:Next.js、React
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年2月15日·更新于 2026年7月23日

打开 DevTools 的 Network 面板,按流量排序,排在第一的那个资源十有八九是图片。这不是错觉——HTTP Archive 的统计反复证明一件事:一个典型网页的总体积里,图片占了一半以上。你把 JS bundle 砍了 30KB 得意半天,结果首屏一张没压缩的 2MB Hero 图把 LCP 直接干到 4 秒。

所以我的观点很直接:图片优化是 Web 性能优化的第一战场,没有之一。它对 LCP 的影响比任何 JS 优化都大,而且投入产出比极高——改几个标签、换一个组件、配一行 CDN 参数,就能砍掉百分之几十的流量。这篇文章把这套事情讲透,从原生 HTML 到 Next.js,给一套能在生产环境直接落地的方案。

一、为什么图片优化等于优化 LCP

LCP(Largest Contentful Paint)测的是首屏最大元素渲染完成的时间。而首屏最大的元素是什么?九成是图片——一张 Hero banner、一张产品大图、一张文章封面。Google 自己的数据:LCP 元素里有超过 70% 是图片。

这意味着两件事:

第一,你的 LCP 天花板基本由图片决定。图片不加载完,LCP 元素就不算"绘制完成"。你把 TTFB 优化到 100ms、把 CSS inline 到 HTML、把 JS 全砍了——只要那张 Hero 图还在慢悠悠地下载,LCP 就上不去。

第二,图片是少数几个你能"四两拨千斤"的资源。JS 优化要重构、要拆包、要改架构;图片优化常常就是换个格式、改个尺寸、加几个属性。一张 1.2MB 的 JPEG 转成 AVIF 可能只剩 180KB,渲染时间直接腰斩。

所以记住这个优先级:先治图片,再谈别的。一个站点如果图片没优化好,去抠 JS 的 50ms 是舍本逐末。

二、格式选型:AVIF、WebP 还是 JPEG/PNG

格式是图片优化的第一刀。同样的画面,不同格式体积能差 2 到 5 倍。先把四类格式摆清楚。

JPEG:老牌有损格式,照片类内容的传统王者。压缩率还行,兼容性 100%。缺点是不支持透明、不支持动画,压缩率被现代格式碾压。

PNG:无损 + 支持透明,适合图标、Logo、需要 alpha 通道的图。但照片类内容用 PNG 体积会爆炸,一张手机拍的照片存成 PNG 可能 10MB 起步。别拿 PNG 存照片,这是新人最常见的浪费。

WebP:Google 推的现代格式,同时支持有损/无损/透明/动画。同等画质下比 JPEG 小 25%–35%,比 PNG 小得更多。兼容性如今已经非常好——主流浏览器全面支持,可以当默认格式用。

AVIF:基于 AV1 视频编码的图片格式,目前压缩率的天花板。同等画质比 JPEG 小 50%,比 WebP 还要小 20% 左右。代价是编码慢(AVIF 编码耗时大约是 WebP 的 1.5 倍),且部分老浏览器(尤其是 IE 和老版 Safari)不支持,需要 fallback。

实战选型我的建议很明确:

默认 WebP,对大流量 Hero 图和首屏图上 AVIF,给老浏览器留 JPEG/PNG 兜底。

关键是怎么"兜底"。答案是 <picture> 元素——浏览器会按 <source> 顺序挑第一个能解码的格式:

html
<picture>
  <source srcset="/hero.avif" type="image/avif" />
  <source srcset="/hero.webp" type="image/webp" />
  <img src="/hero.jpg" alt="首屏主视觉" width="1600" height="900" fetchpriority="high" />
</picture>

支持 AVIF 的浏览器拿第一行,不支持 AVIF 但支持 WebP 的拿第二行,啥都不支持的(基本只剩古董)拿最后的 JPEG。<img> 是必须的兜底,也是无障碍和 SEO 的入口——alt、width、height 都写在它身上。

一个常见误区:很多人用 Accept 头在服务端判断格式然后只返回一个 URL。能跑,但把职责混到了服务端。能用 <picture> 就用 <picture>,它是声明式的、可缓存的、CDN 友好的。

三、响应式图片:srcset + sizes 才是正解

格式解决了"小多少",响应式解决"下哪个尺寸"。这件事比换格式还重要,却最被忽视。

问题场景:你用一张 3840×2160 的原图当 Hero。桌面宽屏看是值的,但用户用 375px 宽的手机打开,浏览器照样下载这张 4K 图——带宽浪费 90%,LCP 直接崩。你不能给所有设备发同一张图。

解决方案是 srcset + sizes。srcset 列出多个尺寸的候选,sizes 告诉浏览器这张图在不同视口下实际显示多宽,浏览器自己挑最合适的下。

html
<img
  src="/hero-1600.jpg"
  srcset="/hero-640.jpg 640w, /hero-960.jpg 960w, /hero-1280.jpg 1280w, /hero-1600.jpg 1600w, /hero-2400.jpg 2400w"
  sizes="(max-width: 768px) 100vw, (max-width: 1280px) 80vw, 1200px"
  alt="首屏主视觉"
  width="1600"
  height="900"
  fetchpriority="high"
  decoding="async"
/>

逐行解释:

  • srcset 里的 640w 表示"这张图实际宽度 640 像素"。w 描述符是宽度,不是密度。
  • sizes 是关键。它告诉浏览器:视口 ≤768px 时图占满屏宽(100vw);768–1280px 时占 80vw;更大时固定 1200px。浏览器拿这个信息和设备的 DPR 一算,就知道该下哪个 srcset 候选。
  • 没有 sizes,浏览器默认按 100vw 算,于是手机也会去下最大的那张。这是响应式图片最常见的坑。

生成这一堆尺寸别手动搞。用构建工具(sharp、vite-imagetools)或 CDN 在流水线里自动产出,原图上传一次,5 个尺寸自动生成。sizes 要根据你的布局写实——你不确定就打开 DevTools 量一下实际渲染宽度,别瞎填。

四、Next.js Image 组件:把上面这些全自动化

前面三节讲的原生方案,每一条都对,但每一条都要你自己维护:生成多尺寸、写 <picture>、配 srcset、算 sizes、做 blur 占位……项目一大就累。Next.js 的 next/image 组件把这些全自动化了,且 Next.js 16 这版有几个关键变化必须知道。

先看最小用法:

jsx
import Image from 'next/image'

export default function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="首屏主视觉"
      width={1600}
      height={900}
      sizes="(max-width: 768px) 100vw, 1200px"
      preload
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,/9j/4AAQ..."
    />
  )
}

它替你做的事:

  1. 自动格式协商。Next.js 服务端看请求的 Accept 头,支持 AVIF 就给 AVIF,否则 WebP,再否则原图。你只管传一张 jpg,输出端自动最优。
  2. 自动响应式 srcset。根据 deviceSizes(默认 [640, 750, 828, 1080, 1200, 1920, 2048, 3840])和你的 sizes 自动生成完整的 srcset。注意:只有传了 sizes 才会生成完整的响应式 srcset;不传只生成 1x/2x 两个。
  3. 自动懒加载。默认 loading="lazy",首屏外的图不会抢带宽。
  4. 自动防 CLS。要求你给 width/height(或用 fill),从源头杜绝布局抖动。

接下来是 Next.js 16 的几个破坏性变化,老项目升级必看:

变化一:priority 被废弃,改用 preload。 这是 v16.0.0 最显眼的改动。以前给 LCP 图加 priority,现在要写 preload。语义更清晰——它会在 <head> 里插入 <link rel="preload">,让浏览器在解析到 <img> 之前就开始下载。

jsx
// ❌ Next.js 16 已废弃
<Image src="/hero.jpg" priority />

// ✅ Next.js 16 正确写法
<Image src="/hero.jpg" preload />

官方文档原话:大多数情况下用 loading="eager" 或 fetchPriority="high" 就够了,不必非得 preload。我的实践:首屏 LCP 图用 preload,其它图一律不管。

变化二:qualities 必须显式配置。 v16 起默认只允许 [75] 一个质量档。你想用别的质量,必须在 next.config.js 里加白名单:

js
// next.config.js
module.exports = {
  images: {
    qualities: [25, 50, 75, 100],
    formats: ['image/avif', 'image/webp'],
    remotePatterns: [new URL('https://cdn.example.com/**')],
  },
}

这是个安全收紧——防止恶意用户构造任意 quality 参数把你的图片服务当免费 CPU 算力使。升级 16 时如果项目里用了非默认 quality,漏配这个会直接 400。

变化三:remotePatterns 彻底取代 domains。 domains 早在 v14 就废弃了,v16 更是别想用。远程图必须在 next.config.js 配 remotePatterns,建议尽量精确(带 pathname、search),别图省事写 **:

js
images: {
  remotePatterns: [
    { protocol: 'https', hostname: 'cdn.example.com', pathname: '/images/**', search: '' },
  ],
}

关于格式配置。 默认 formats: ['image/webp']。强烈建议显式开 AVIF:

js
images: { formats: ['image/avif', 'image/webp'] }

数组顺序有意义——第一个匹配上的优先。AVIF 编码慢(比 WebP 慢 50%)但体积小 20%,Next.js 会把每个格式分别缓存,首次请求慢一点,后续命中缓存就快了。自托管时务必让前面的代理/CDN 把 Accept 头透传给 Next.js,否则格式协商会失灵,这一点坑了无数人。

关于 fill。 当你不知道图的实际宽高(比如铺满一个容器),用 fill 让图撑满父容器,但父容器必须 position: relative,且务必配 sizes,否则按 100vw 算会下超大的图:

jsx
<div style={{ position: 'relative', width: '100%', height: '400px' }}>
  <Image
    src="/banner.jpg"
    alt="横幅"
    fill
    sizes="(max-width: 768px) 100vw, 1200px"
    style={{ objectFit: 'cover' }}
  />
</div>

关于静态导入。 把图 import 进来当 src,Next.js 会在构建期分析文件,自动填 width/height、自动生成 blurDataURL(jpg/png/webp/avif 都行)。这是最省心的用法,适合数量可控的本地图:

jsx
import hero from '@/public/hero.jpg'

<Image src={hero} alt="首屏" placeholder="blur" sizes="100vw" />

远程图就没这待遇,blurDataURL 得自己生成——用 plaiceholder 之类的库在后端算一个 10px 的缩略图转 base64,塞给前端。注意别让它太大,一个模糊占位图几 KB 就够了。

五、懒加载与占位:别让用户盯着白屏

图片优化不只是少下字节,还要让用户感觉快。两件事:懒加载控制"什么时候下",占位控制"下之前看见什么"。

懒加载。 现代浏览器原生支持,一行搞定:

html
<img src="/photo.jpg" loading="lazy" decoding="async" width="800" height="600" alt="..." />

loading="lazy" 让浏览器在图接近视口时才开始下载,首屏外的图不抢带宽。decoding="async" 让解码不阻塞主线程。这两个属性零成本,加上就完事。

但有一条铁律:LCP 图绝不能懒加载。 这是新手最容易犯的错——把首屏那张 Hero 图也写上 loading="lazy",结果浏览器等它进入视口才开始下,白白浪费了几百毫秒。LCP 图要 loading="eager"(或不写,默认 eager)加 fetchpriority="high",让它最早进请求队列。

Next.js 里 next/image 默认就是 lazy,所以反过来——LCP 图要显式 preload 提前加载,破除默认懒加载。

占位。 懒加载的图在加载前是一块空白,布局好的话是一块灰色区域,体验上像"页面没加载完"。占位就是给这块区域先填点东西,让用户知道"这里会有图"。

最有效的占位是 placeholder="blur"——先显示一张极度模糊的小图,加载完再切到清晰图。视觉上像图在"对焦",比白屏高级太多:

jsx
<Image src={hero} alt="首屏" placeholder="blur" />

静态导入自动生成 blurDataURL;远程图自己传。还有一招是用纯色或 CSS 渐变占位(通过 style 设背景色),虽然不如 blur 优雅,但零成本、零额外请求,适合大量列表图。

如果想更花哨——shimmer 动画、骨架屏,可以把 blurDataURL 换成一个小的 SVG data URL,或者外面包一层 CSS 动画的容器。但记住:占位的目的是"消除白屏的焦虑",不是炫技,别为了 shimmer 多写 50 行 CSS。

六、CDN 动态处理:把图片管线挪到边缘

到这一步,前面的方案都要你在"构建期"生成多格式、多尺寸。但很多场景下,图是用户上传的、是 UGC、是商品图——你不可能在构建期处理。这时候把图片管线挪到 CDN,按需动态处理,是生产级方案。

典型玩法:图床只存一张原图,CDN 在请求时按参数裁剪、转格式、调质量。以 Cloudinary / Imgix / 阿里云 OSS 图片处理 / 腾讯云数据万象 这类服务的 URL 参数模式为例:

text
https://cdn.example.com/hero.jpg?w=800&h=600&q=75&format=auto

format=auto 让 CDN 看 Accept 头自动返回 AVIF/WebP/JPEG;w/h 控制尺寸;q 控制质量。同一个原图,桌面拿 ?w=1600,手机拿 ?w=640,现代浏览器拿 AVIF,老浏览器拿 JPEG。一份原图,无数种产出。

在 Next.js 里,这种场景用自定义 loader 把 next/image 的请求指向你的 CDN:

js
// next.config.js — 关掉默认优化服务,改用 CDN
module.exports = {
  images: {
    loader: 'custom',
    loaderFile: './image-loader.js',
    formats: ['image/avif', 'image/webp'],
  },
}
js
// image-loader.js
'use client'
export default function cdnLoader({ src, width, quality }) {
  return `https://cdn.example.com${src}?w=${width}&q=${quality || 75}&format=auto`
}

这样 next/image 依然给你算 srcset、管懒加载、防 CLS,但实际的转码工作全交给 CDN 边缘节点,你的 Next.js 服务不背 CPU 负担。这是高流量站点的推荐架构——自建 Next.js 图片优化服务在流量上来后会成为瓶颈和成本黑洞,CDN 才是图片处理的归宿。

几点经验:

  • 缓存是命门。 CDN 转码结果要长缓存(Cache-Control: max-age=31536000, immutable),参数变了就换 URL 或加版本号。Next.js 默认 minimumCacheTTL 是 14400 秒(4 小时),高缓存场景可以调到 31 天。
  • 质量别设太高。 70–80 对照片类内容肉眼几乎无差别,体积却差一截。q=75 是 Next.js 默认值,也是性价比最高的档。
  • 别让用户随意传参数。 如果 URL 参数直接暴露给前端拼,要限制允许的 w/q 集合(Next.js 16 的 qualities 白名单就是这个思路),否则有人拿你的 CDN 当免费图片处理 API 用。

七、避坑指南:CLS 与那些"看似无关"的细节

最后讲几个踩过才会记住的坑。

坑一:不设 width/height 导致 CLS。 这是头号大坑。图片如果不给尺寸,浏览器不知道该给它留多大空间,于是先按 0 高度排版;图下载完才知道多大,"啪"一下把下方内容推开——CLS 爆表,用户还以为页面在抽搐。

解法永远只有一条:所有 <img> 必须有 width 和 height,让浏览器提前算出宽高比预留空间。CSS 控制实际渲染尺寸,width/height 只管声明比例:

html
<img src="/photo.jpg" width="800" height="600" alt="..." />
<!-- 渲染尺寸用 CSS 控制,比例由 width/height 锁定 -->

不想算宽高就用 aspect-ratio:

css
.hero img { width: 100%; aspect-ratio: 16 / 9; height: auto; }

Next.js next/image 强制要 width/height 或 fill,就是在防这个坑。这是它最大的价值之一。

坑二:LCP 图被设成懒加载。 第五节说过了,再强调一遍:首屏 Hero 图 = preload + fetchpriority="high",别 loading="lazy"。

坑三:远程图不配 remotePatterns。 Next.js 升级后突然所有远程图 400,十有八九是 domains 被废、remotePatterns 没配。

坑四:代理/CDN 没透传 Accept 头。 自托管 Next.js,前面套了 Nginx/CDN,结果所有请求都拿到 WebP 甚至原图,AVIF 协商失效。确保 Accept 头一路透传到 Next.js 进程。

坑五:fill 不配 sizes。 浏览器按 100vw 算,手机上下到 3840px 宽的巨图,流量和 LCP 双双崩。fill 几乎总要配 sizes。

坑六:SVG 不该走图片优化管线。 SVG 是矢量,无损缩放,优化它没意义还可能引入安全风险(它内嵌脚本)。Next.js 里 SVG 用 unoptimized,或直接当普通 <img>。需要时才开 dangerouslyAllowSVG 并配好 CSP。

坑七:blurDataURL 过大。 有人把整张图的 base64 塞进去当占位,HTML 体积被撑爆。blur 占位就该是 10px 级别的小图,几 KB 以内。

小结

  • 图片是页面最大的资源,优化图片约等于优化 LCP。 LCP 元素七成是图片,先把图片治了再谈 JS 优化,别本末倒置。
  • 格式选型有明确顺序:AVIF > WebP > JPEG/PNG。 默认 WebP,大流量首屏图上 AVIF,用 <picture> 给老浏览器留 JPEG 兜底。Next.js 里配 formats: ['image/avif', 'image/webp']。
  • 响应式图片的核心是 srcset + sizes,缺一不可。 没了 sizes 浏览器按 100vw 算,手机照样下巨图。Next.js 里只要用了 fill 或响应式布局,就必须写 sizes。
  • Next.js 16 有三个必记的变化:priority 换成 preload、qualities 要显式配白名单、remotePatterns 彻底取代 domains。 老项目升级先把这三处扫一遍。
  • CLS 的根因永远是没设尺寸。 所有图必须 width/height 或 aspect-ratio,LCP 图别懒加载,自托管记得透传 Accept 头。高流量站点把转码挪到 CDN,别让 Next.js 服务背 CPU。

一句话收尾:图片优化不是抠几个 KB 的玄学,是一套从格式、尺寸、加载时机到边缘管线的系统工程——把这张系统图刻进脑子,你的 LCP 和用户的流量账单,会同时谢谢你。

Related

相关推荐