图片把首页拖到八秒才加载完:srcset 响应式图片与预加载优先级的改造记录
上个月接手一个做工业阀门出口的独立站:首页 HTML 只有 46KB,图片却有 24 张未压缩原图,单张 3-6MB,总量 89MB。移动端 Largest Contentful Paint(最大内容绘制,LCP)实测 8.1 秒,Google PageSpeed Insights 移动端 23 分。客户反映询盘表单页流量没掉,但首页自然流量半年跌了三成——跳出率先上去,排名跟着掉,这是典型的性能拖垮 SEO 的路径。这篇记录我们把首页图片链路整体改造到 LCP 1.9 秒的完整过程,重点放在 srcset、格式转换和预加载优先级这三件事上。
一、先量化:问题到底在哪
动手前先跑数据。用 PageSpeed Insights(PSI)和 Lighthouse 11 分别测了改造前的移动端与桌面端,再在 Chrome DevTools 的 Network 面板里按 LCP 元素过滤,拿到第一手结论:

| 指标 | 改造前(移动端) | 说明 |
|---|---|---|
| LCP | 8.1s | LCP 元素是首屏 banner 主图,原图 5.8MB |
| 首页图片总传输量 | 89MB | 24 张原图全部按桌面尺寸下发 |
| PSI 性能分(移动) | 23 分 | LCP、TBT、CLS 全红 |
| 跳出率(移动,30 天均值) | 68.4% | GA4 后台数据,非估测 |
| 4G 模拟下首字节后图片完成时间 | 9.6s | 图片串行排队,无并行优化 |
三个结构性问题很清楚:
- 所有图片只有一张 2560px 宽的原图,手机、平板、桌面拿到的是同一份文件,没有响应式降级。
- LCP 那张 banner 图排在 DOM 后面,浏览器要等 CSS 解析完、预扫描到
<img>才开始下载,预加载优先级是 Low。 - 原图是相机直出的 JPEG,没有做任何有损压缩和现代格式转换。
二、srcset 与 sizes:让每台设备只下载它需要的图
2.1 改造前的写法与改造后的写法
改造前所有图片统一这样写:
<!-- 改造前:单源图片,任何设备都拉 2560px 原图 -->
<img src="/img/valve-hero.jpg" alt="不锈钢闸阀产品全景">
改造后换成 srcset + sizes 的标准写法。这里用的是固定宽度断点方案(width descriptors),因为我们能精确控制服务端生成的每一个尺寸:
<!-- 依赖:无构建依赖,原生 HTML5 标准写法,兼容 Chrome 38+ / Safari 10+ / Firefox 38+ -->
<!-- 第一步:给 LCP 主图声明 srcset,按宽度描述符列出服务端实际存在的四个尺寸 -->
<!-- src 是不支持 srcset 的老浏览器兜底,同时帮助爬虫确定规范地址 -->
<!-- sizes 告诉浏览器这张图在视口里占多宽,浏览器据此挑最小够用的文件 -->
<!-- 首屏主图:显式拉高获取优先级,配合后面的 preload 使用 -->
<img
src="/img/valve-hero-1280.webp"
srcset="/img/valve-hero-640.webp 640w,
/img/valve-hero-960.webp 960w,
/img/valve-hero-1280.webp 1280w,
/img/valve-hero-1920.webp 1920w"
sizes="(max-width: 640px) 100vw,
(max-width: 1280px) 92vw,
1200px"
fetchpriority="high"
decoding="async"
alt="不锈钢闸阀产品全景">
下面那张产品目录图在页尾,写法完全不同——懒加载兜底,优先级放低:
<!-- 第二步:非首屏图片用懒加载,浏览器会在进入视口前约 1250px 时才请求 -->
<!-- 固定 width/height 防止图片加载时布局偏移,保住 CLS 指标 -->
<img
src="/img/catalog-800.webp"
loading="lazy"
decoding="async"
width="800"
height="600"
alt="2026 产品目录封面">
2.2 一个踩过的坑:sizes 写错会让优化反向生效
改造第一版时,我把 banner 的 sizes 写成了 sizes="100vw",结果 4K 桌面浏览器认为需要 3840px 宽的候选文件,直接选了 1920w 那张再放大,PSI 报了一个 "Larger than necessary images" 警告。sizes 必须如实描述图片的实际布局宽度,弹窗里半屏显示就写 50vw 加媒体查询,不能图省事写满屏。
三、原理剖析:浏览器怎么选 srcset 候选、优先级怎么排
这一节把两个机制讲透,后面所有配置都建立在这套逻辑上。
候选选择机制:浏览器拿到 srcset 后,先算出「当前布局下需要的像素宽度 = sizes 解析值 × DPR(设备像素比)」,然后从候选列表里选宽度大于等于这个值的最小那张;所有候选都不够大时才选最大的。所以 640w 文件的存在意义,就是服务 375×2 DPR 的 iPhone 这类设备——它需要 750px,选 960w 就浪费了 27% 的字节。
预加载优先级机制:Chromium 给资源排队时分 High/Medium/Low/Lowest 几档。普通 <img> 默认 Low;loading="lazy" 压到 Lowest;<link rel="preload"> 提到 High;fetchpriority="high" 同样提到 High,且 preload 加上 imagesrcset 后,预加载和实际渲染选中的是同一个文件,不会出现「预加载了一张、渲染时又下了另一张」的双倍请求。
flowchart TD
A[浏览器解析到 img 元素] --> B{有 srcset 与 sizes?}
B -- 否 --> C[直接下载 src 指定的单一文件]
B -- 是 --> D[计算 需求宽度 = sizes 解析值 × DPR]
D --> E[在候选列表中选 ≥ 需求宽度的最小文件]
E --> F{元素是否 loading=lazy?}
F -- 是 --> G[优先级 Lowest<br>距视口约 1250px 才发起请求]
F -- 否 --> H{fetchpriority 属性?}
H -- high --> I[优先级 High<br>进入预加载扫描队列]
H -- 未设置 --> J[优先级 Low<br>CSS 加载完才被预扫描发现]
改造前的致命点在 J 这个分支:banner 图没有 preload、没有 fetchpriority,Lighthouse 追踪显示它的请求在 TTFB 之后 2.3 秒才发出,因为要先等渲染阻塞的 CSS 和字体 promise。这就是 8.1 秒里最大的一块死时间。
四、首屏预加载:preload + imagesrcset + fetchpriority 三件套
只写 srcset 还不够——预加载扫描器发现的仍然是 Low 优先级。对 LCP 元素,要在 <head> 里显式提前:
<!-- 依赖:HTTP/2 环境(Cloudflare 托管),preload 在 HTTP/1.1 下收益打折 -->
<!-- 第三步:在 head 中预加载 LCP 图片,imagesrcset 与正文 img 的 srcset 逐字一致 -->
<!-- imagesrcset/imagesizes 是 preload 专属属性,与 srcset/sizes 语义相同 -->
<!-- fetchpriority 提升该预加载请求为 High,HTML 尚未解析到 img 就开始下载 -->
<link
rel="preload"
as="image"
imagesrcset="/img/valve-hero-640.webp 640w,
/img/valve-hero-960.webp 960w,
/img/valve-hero-1280.webp 1280w,
/img/valve-hero-1920.webp 1920w"
imagesizes="(max-width: 640px) 100vw,
(max-width: 1280px) 92vw,
1200px"
fetchpriority="high">
三件套的分工边界要划清楚,这也是这次改造中我们内部对齐最多的一点:
| 属性 | 作用对象 | 解决的问题 | 不要用在哪 |
|---|---|---|---|
| preload + imagesrcset | <head> 里的链接 |
HTML 解析到 img 之前就发起请求 | 非首屏图片(会挤占带宽) |
| fetchpriority="high" | img 或 preload | 在多资源竞争时插队 | 页面里超过 1-2 张,多了等于没提 |
| loading="lazy" | img | 推迟视口外图片的请求 | LCP 元素(会拖慢触发时机) |
改造完成后抓了一次 HAR:banner 图请求从 TTFB 后 2.3s 提前到 0.4s 发出,与 CSS 并行下载,图片本身 5.8MB JPEG 换成 1280w WebP 后只剩 96KB。
五、格式转换:WebP 与 AVIF 的批量生产线
手工转 24 张图不可持续,我们搭了一条脚本化流水线:源图入库后自动产出四个宽度 × 两种格式共八份文件。移动端 Chrome 优先吃 AVIF(比同画质 WebP 再省 20-30%),Safari 16 以下回落到 WebP。
flowchart LR
A[原图入库<br>JPEG 2560px] --> B[Pillow 统一压缩<br>质量 85]
B --> C{按四档宽度缩放}
C --> D[640w] & E[960w] & F[1280w] & G[1920w]
D & E & F & G --> H[AVIF 转换<br>cwebp / avifenc]
D & E & F & G --> I[WebP 转换<br>cwebp -q 82]
H & I --> J[写入 CDN 源站目录<br>按命名规范归档]
5.1 Python 批量脚本(Pillow 10.2 + cwebp 1.3)
# 依赖环境:Python 3.11 + Pillow 10.2.0(pip install pillow),cwebp 1.3.2 需单独安装并在 PATH 中
# 用途:把原图目录批量缩放为四档宽度并转出 WebP,AVIF 交给 cwebp 之后的 avifenc 处理
# 前提:raw_images 目录与 dist/img 目录已建好,脚本在项目根目录执行
import subprocess
from pathlib import Path
from PIL import Image
SRC = Path("raw_images")
OUT = Path("dist/img")
WIDTHS = [640, 960, 1280, 1920] # 与 srcset 候选列表严格对应的四档宽度
for src in SRC.glob("*.jpg"):
im = Image.open(src).convert("RGB") # 相机原图可能带 alpha 或 CMYK,先统一转 RGB
for w in WIDTHS:
ratio = w / im.width
if ratio >= 1:
continue # 原图小于目标宽度时跳过,避免放大失真
resized = im.resize((w, int(im.height * ratio)), Image.LANCZOS)
dst = OUT / f"{src.stem}-{w}.webp"
# LANCZOS 是缩放质量的默认选择,BILINEAR 更快但边缘锯齿明显
resized.save(dst, "WEBP", quality=82, method=6) # method=6 压缩更慢但体积更小
print(f"done: {src.name}")
# 校验产物:逐个文件打印体积,人工抽查画质(尤其渐变背景是否有 banding)
# 提示:method=6 只影响编码速度不影响解码,CDN 分发侧无额外成本
for f in sorted(OUT.glob("*.webp")):
print(f.name, f.stat().st_size // 1024, "KB")
同一台服务器上跑着 .NET 的图片处理服务,对外贸站的定制品图走 ImageSharp 管线,逻辑等价:
// 依赖环境:.NET 8 + SixLabors.ImageSharp 3.1.2(NuGet 包名 ImageSharp)
// 场景:后台编辑上传定制品图时,同步产出各档位 WebP 副本
// 注意:Clone 后的 Resize 不会污染原始 image 对象,循环内可安全复用
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Processing;
int[] widths = { 640, 960, 1280, 1920 }; // 档位与前端 srcset 保持同一份常量
using var image = Image.Load(inputStream); // 输入流来自后台上传的原始 JPEG
foreach (var w in widths)
{
if (w >= image.Width) continue; // 目标档位超过原图宽度则不放大
var clone = image.Clone(ctx => ctx.Resize(w, 0, KnownResamplers.Lanczos3));
// 高质量编码模式:quality 82 与 Python 管线口径一致,避免两套画质标准
// FileFormat 指定 Lossy:默认值可能按内容自动选择,会导致体积不可控
clone.SaveAsWebp(outputStream, new WebpEncoder { Quality = 82, FileFormat = WebpFileFormatType.Lossy });
}
转换前后的体积对比(同一张 5.8MB banner 原图):
| 格式与宽度 | 传输体积 | 相对原图节省 |
|---|---|---|
| JPEG 2560w(原图) | 5800 KB | — |
| WebP 1920w | 214 KB | 96.3% |
| WebP 1280w | 96 KB | 98.3% |
| AVIF 1280w | 71 KB | 98.8% |
5.2 命令行兜底
偶尔有运营临时上传的图来不及走脚本,直接在服务器上手工转:
# 依赖环境:libwebp 1.3.2(含 cwebp)、libavif 1.0(含 avifenc),Windows 下用 scoop 安装
# 提示:两条命令都只转一张图,批量场景仍以第五节的 Python/.NET 管线为准
# 单图转 WebP:-q 82 是我们实测画质与体积的平衡点,低于 80 渐变背景出现色带
cwebp -q 82 -resize 1280 0 raw_images/valve-hero.jpg -o dist/img/valve-hero-1280.webp
# 单图转 AVIF:speed 5 转换速度与压缩率的折中,服务器批量任务用 speed 4
avifenc -q 55 -s 4 --resize 1280 -1 raw_images/valve-hero.jpg dist/img/valve-hero-1280.avif
六、CDN 层:Cloudflare 的图片压缩与缓存配置
源站只出一份 WebP 主版本,AVIF 协商交给 Cloudflare 做,这样源站带宽压力最小:
- Speed → Optimization → Image Optimization:开启 Polish(Lossy 模式)与 WebP 转换,命中缓存的 JPEG 请求自动回落成 WebP 下发。
- Cache Rules:
/img/*设置 Edge TTL 30 天、浏览器 TTL 7 天,版本迭代靠文件名里的宽度后缀与 git hash 区分,不做 purge。 - HTTP/3 与 Brotli:全局开启;图片走 HTTP/3 连接复用后,24 张图的排队时间从 1.1s 降到 0.4s。
- 关闭了 Mirage 和 Rocket Loader——前者会二次改写图片请求造成闪烁,后者延迟了首屏脚本反而推高 TBT。
七、改造前后:数据摆在一起
全链路上线两周后(等 GA4 移动端 28 天数据窗口补齐),重新跑了同一套测量口径:
| 指标 | 改造前 | 改造后 | 变化 |
|---|---|---|---|
| LCP(移动端,PSI 实测) | 8.1s | 1.9s | -76.5% |
| INP(移动端) | 320ms | 180ms | -43.8% |
| CLS(移动端) | 0.21 | 0.02 | 图片补齐 width/height 后消除偏移 |
| 首页图片总传输量(移动) | 89MB | 1.8MB | -98% |
| PSI 性能分(移动) | 23 | 94 | 红转绿 |
| PSI 性能分(桌面) | 58 | 99 | 红转绿 |
| 移动端跳出率(28 天均值) | 68.4% | 47.2% | -21.2 个百分点 |
| 首页自然点击(8 周后 vs 前 8 周) | 基线 | +34% | 与同期产品页点击持平增长 |
Core Web Vitals(核心网页指标)三项全部进入「良好」区间,Search Console 的 CWV 报告从「失败」翻绿用了 28 天。更要紧的是商业指标:移动端跳出率降到 47.2%,意味着每 100 个自然进来的访客多留下约 21 个——对 B2B 询盘型独立站,这比任何一次关键词微调带来的 SEO 收益都直接。这次改造之后,「网站优化先查资源体积」被写进了团队的上线路径,网站优化的检查清单里,图片链路从「上线前有空再看」改成了「发布流水线的必过闸口」。
回头看这条改造路径,传统搜索引擎爬取与排名对页面速度的权重这几年只增不减;图片写法规范之后,同样的结构化语义和内容质量更容易被生成式搜索引用,算是顺手为 GEO(生成式引擎优化)打好了地基。
参考与延伸
- Google Developers:图片优化指南与 LCP 优化实践 — https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/image-optimization
- MDN:响应式图片(srcset 与 sizes 详解) — https://developer.mozilla.org/zh-CN/docs/Web/HTML/Responsive_images
- web.dev:预加载响应式图片(preload 与 imagesrcset) — https://web.dev/articles/preload-responsive-images
- Google Search Central:Core Web Vitals 对搜索的影响 — https://developers.google.com/search/docs/appearance/core-web-vitals
关键词:SEO、网站优化、srcset、图片懒加载、预加载优先级、WebP、Core Web Vitals、前端性能