侧边栏壁纸
  • 累计撰写 235 篇文章
  • 累计收到 2 条评论

为了优化 LCP 全站改造现代响应式图片(Picture 与 AVIF/WebP),搜索收录与快照出图率却彻底归零?排查 Baiduspider 忽视 source 标签致配图蒸发、Nginx 缺失 Vary: Accept 导致 CDN 缓存投毒、sizes 视口失控打爆爬虫渲染超时与图片变体榨干抓取配额我踩过的几个深坑

2026-10-26 / 0 评论 / 7 阅读
AI 摘要由 AI 生成

文章探讨了全站改造现代响应式图片后,搜索收录与快照出图率归零的问题。指出百度爬虫忽视source标签、Nginx配置缺失Vary: Accept导致CDN缓存投毒等问题,并提出解决方案,强调兼容所有爬虫与现代格式的写法需保证底部img标签持有真实、高清晰度、格式通用的原始图片回退地址。

上个月我复盘站点的 Core Web Vitals 评分,移动端 LCP(最大内容绘制)一直在 3.4 秒左右晃荡。灯塔工具(Lighthouse)里排在最前列的优化建议非常显眼:以新一代格式提供图片、适当调整图片尺寸。

站点里存着上千篇带图的技术排查记录,历史配图全是最原始的 PNG 和 JPEG,单图体积经常超出 1MB。为了把 LCP 压到 2.0 秒以内的绿线标准,我花了一个周末把整套图片分发逻辑重写了一遍:后端写批处理把图库全量转出了 AVIF 和 WebP,前端把原本的 img 标签改造成了 picture 容器并附带 srcset 与 sizes,Nginx 网关层也配了基于客户端请求头的格式协商。

改造上线的当天下午,Chrome 里测试 LCP 直接降到了 1.6 秒,首屏图片渲染体验提升明显。结果到了第二个礼拜看监控和站长后台,数据直接把我看傻了:百度搜索结果里的文章缩略出图率从原来的 85% 掉到了几乎看不见;老版本 Safari 和微信内置浏览器频繁弹出破图报错;Googlebot 的渲染抓取日志里出现了大面积的资源超时拦截;更麻烦的是,新发文章的收录节奏基本停了,爬虫配额全消耗在同一张图片的各种尺寸变体上。

前端规范和搜索引擎的爬虫实现之间存在不少偏差。这里把当时排查出来的几个深坑和调整方案记录下来。

百度爬虫只提取 img 的 src,对 picture 内部的 source 标签直接视而不见

现代前端框架和很多性能优化教程推荐的 Picture 结构通常长这样:

<picture>
  <source type="image/avif" srcset="/uploads/arch-800.avif 800w, /uploads/arch-1200.avif 1200w">
  <source type="image/webp" srcset="/uploads/arch-800.webp 800w, /uploads/arch-1200.webp 1200w">
  <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4MDAiIGhlaWdodD0iNDUwIj48L3N2Zz4=" 
       alt="系统架构流转图" 
       width="800" 
       height="450" 
       loading="lazy">
</picture>

当时为了配合前端懒加载库,底层 img 的 src 填入了一段 1x1 的透明占位 base64,或者直接放了一个极小的灰色骨架 SVG,真正的图片 URL 全部分发到了 source 标签的 srcset 中。

桌面端现代浏览器对这段 HTML 解析得毫无问题,能根据浏览器的解码能力自动挑选最优先的 AVIF,挑不了就挑 WebP。然而国内搜索引擎爬虫(尤其是百度图片搜索索引组件)的网页抽取逻辑并没有运行完整的高版本无头浏览器渲染管线。百度的图文关系抽取程序大多是轻量级的 HTML 词法解析器,抓取策略非常机械:直接在 DOM 里寻找 img[src] 选择器,读取属性里的字符串作为图片 URL。

遇到上面的写法时,百度爬虫抓到的图片地址是一串 data:image/svg+xml;base64。解析程序认定这个地址是无效资源或者占位符,直接把整张配图丢弃。搜索结果列表在组装图文卡片时找不到有效主图,原本该展示三图或者单图缩略图的位置全部变成了纯文本列表,点击率瞬间腰斩。

哪怕把 src 换成真实存在的低清缩略图,只要图片分辨率过低(比如低于 200x200),同样会被搜索引擎的质量模型过滤掉。

兼容所有爬虫与现代格式的写法必须保证:底部的 img 标签必须持有真实、高清晰度、且格式通用的原始图片回退地址(比如标准 JPEG 或 PNG),绝不能拿占位符充数:

<picture>
  <source type="image/avif" srcset="/uploads/arch-800.avif 800w, /uploads/arch-1200.avif 1200w" sizes="(max-width: 768px) 100vw, 800px">
  <source type="image/webp" srcset="/uploads/arch-800.webp 800w, /uploads/arch-1200.webp 1200w" sizes="(max-width: 768px) 100vw, 800px">
  <img src="/uploads/arch-800.jpg" 
       srcset="/uploads/arch-800.jpg 800w, /uploads/arch-1200.jpg 1200w"
       sizes="(max-width: 768px) 100vw, 800px"
       alt="系统架构流转图" 
       width="800" 
       height="450" 
       decoding="async">
</picture>

把原始 JPEG 稳妥地挂在 img[src] 上,现代浏览器依然会优先解析 picture 顶部的 source 加载高压缩比的 AVIF,而遇到简易爬虫或者不支持现代格式的古老客户端时,兜底的 img 能够直接提供符合规范的原图。

Nginx 做透明内容协商漏配 Vary: Accept,引发 CDN 缓存投毒

由于站内有海量旧文章,逐篇修改数据库里的 HTML 代码工作量很大。我当时在 Nginx 层面做了一套基于客户端请求头的内容协商(Content Negotiation):URL 保持传统的 /uploads/pic.jpg 不变,Nginx 检查客户端的 Accept 请求头,如果里面包含 image/avif,就偷偷把磁盘上的 pic.jpg.avif 读出来返回。

当时的 Nginx 配置片断:

map $http_accept $img_suffix {
    default "";
    "~*image/avif" ".avif";
    "~*image/webp" ".webp";
}

server {
    listen 443 ssl http2;
    server_name www.llbbs.cn;

    location ~* ^/uploads/(.+\.(jpe?g|png))$ {
        root /data/www/blog;
        try_files $uri$img_suffix $uri =404;
        expires 30d;
        add_header Cache-Control "public, no-transform";
    }
}

本地用最新版 Chrome 测试没有任何问题,访问 /uploads/demo.jpg 拿到的是体积减少了 60% 的 AVIF 字节流。

然而网站前面套了全站 CDN。HTTP 缓存规范规定:如果同一个 URL 会根据客户端的请求头返回不同的内容实体,源站必须在响应头里附带 Vary 字段,告诉代理服务器不能单凭 URL 做缓存键。

上面的 Nginx 配置里完全漏掉了 Vary: Accept。后果非常惨烈:

  1. 某台安装了 Chrome 的访客设备率先访问了 /uploads/demo.jpg,请求头携带 Accept: image/avif。
  2. CDN 边缘节点回源把 AVIF 格式的数据拉了下来,并以 URL /uploads/demo.jpg 作为 Cache Key 存在了边缘节点内存中。
  3. 接着 Baiduspider 或者老款 iPhone 用户访问同一张图片。客户端的请求头只支持普通 JPEG,但 CDN 边缘节点命中了缓存,直接把 AVIF 的二进制内容原封不动地推了回去。
  4. 爬虫或客户端拿着 JPEG 解码器去解析 AVIF 二进制文件,由于魔数签名不匹配,直接抛出解码失败异常。爬虫渲染引擎判定这张图片破损,整页快照出现红叉。

排查日志时能清晰看到,很多爬虫访问图片返回的 Content-Type 还是 image/jpeg,但实际传输的字节数和文件头全是 AVIF 的 00 00 00 1c 66 74 79 70 61 76 69 66。

修复方案是在 Nginx 返回这类动态协商资源时,强制打上 Vary: Accept,同时注意 always 指令修饰符:

map $http_accept $img_suffix {
    default "";
    "~*image/avif" ".avif";
    "~*image/webp" ".webp";
}

location ~* ^/uploads/(.+\.(jpe?g|png))$ {
    root /data/www/blog;

    # 告诉 CDN 必须针对不同的 Accept 头分别缓存不同的版本
    add_header Vary "Accept" always;
    add_header Cache-Control "public, max-age=2592000, no-transform" always;

    # 针对不同后缀重写并匹配正确的 MIME 类型
    try_files $uri$img_suffix $uri =404;
}

如果 CDN 厂商的节点默认不支持基于 Vary 头进行二级缓存切分(国内部分 CDN 服务商默认忽略 Vary 头),就必须彻底放弃同一 URL 隐式返回不同格式的做法,老老实实在 URL 里体现文件格式后缀,走前面提到的前端 Picture 显式声明路线。

sizes 属性偷懒写 100vw,导致爬虫渲染视口超额拉取大图引发超时

为了让图片自适应不同宽度的屏幕,很多开发者会在 img 上写 srcset:

<img src="/uploads/pic-800.jpg"
     srcset="/uploads/pic-400.jpg 400w,
             /uploads/pic-800.jpg 800w,
             /uploads/pic-1600.jpg 1600w,
             /uploads/pic-2400.jpg 2400w"
     sizes="100vw"
     alt="架构图">

这里的坑在于 sizes="100vw"。浏览器根据这个属性计算该下哪张图。如果网页的主内容区域在桌面端被限制在 800px 宽度,而你写了 100vw,浏览器会误以为图片需要撑满整个屏幕宽度。

更关键的是搜索引擎的渲染管线行为。Googlebot 的网页渲染服务(WRS)在模拟无头 Chromium 时,桌面端会模拟较大分辨率的视口,并且为了测试移动端兼容性,部分渲染节点会启用 2x 甚至 3x 的设备像素比(DPR)。

当 WRS 遇到 sizes="100vw" 加上 2x DPR 模拟时,计算得出的图片候选宽度直接奔着 2400px 以上去了,于是它老老实实去下载那个最庞大的 pic-2400.jpg。

Googlebot 官方文档里写过明确的资源调度限制:渲染服务单页抓取的网络资源总量存在上限(通常在 15MB 左右),单次抓取也有固定的超时等待窗口。

一张 2400w 的高清配图往往有数兆体积。页面里如果有三四张插图,WRS 的抓取预算在下载这几张超大图时被迅速耗尽。网络带宽和等待时间超限后,WRS 会直接中断后续请求。结果排在 HTML 后半截的正文、关键外链样式表以及底部的相关推荐组件全部没能完成加载。

爬虫索引到的快照只剩半截内容,核心关键词大量丢失,最终被算法归入内容残缺页面并降权。

解决办法是严谨计算图片的真实物理渲染边界,写出真实的媒体查询尺寸:

<img src="/uploads/pic-800.jpg"
     srcset="/uploads/pic-400.jpg 400w,
             /uploads/pic-800.jpg 800w,
             /uploads/pic-1200.jpg 1200w"
     sizes="(max-width: 840px) calc(100vw - 40px), 800px"
     alt="架构图"
     width="800"
     height="450"
     loading="lazy"
     decoding="async">

正文配图的最大展示宽度既然被 CSS 限死在 800px,那么在大于 840px 的屏幕上直接指明渲染尺寸就是 800px。此时哪怕算上 2x 屏幕,浏览器最高也只会拉取 1200w 或 1600w 的候选图,绝不会盲目向服务端请求 2400w 甚至 4K 的原始文件。

动态图片转码参数炸出海量 URL 变体,榨干站点抓取配额

有的站点使用对象存储的图片处理服务(如 OSS、COS 或各类图片 CDN),支持通过 URL 传参动态缩放:

/uploads/cover.jpg?x-oss-process=image/resize,w_375
/uploads/cover.jpg?x-oss-process=image/resize,w_414
/uploads/cover.jpg?x-oss-process=image/resize,w_768

前端有些自动化打包脚本或者响应式组件,会根据页面不同容器宽度动态拼出很多细碎的参数值。

普通用户浏览时可能只加载其中一两个尺寸。但对搜索引擎爬虫来说,URL 中带问号的每一个不同字符串,在默认情况下都会被判定为一个全新的独立资源链接。

上线这段逻辑后的一周里,我在服务器访问日志里发现:Baiduspider 和 Googlebot 每天几千次的抓取配额,有将近七成全被耗费在抓取同一张图的几十个微小尺寸参数上。爬虫整天在不同宽度的图片参数迷宫里打转,真正更新了文章正文的新页面反而排在队列后方无法得到调度。

针对这种情况,需要做三道收口防线:

  1. 前端严禁动态生成任意像素值的尺寸参数,全站统一规范为 3 个固定档位(比如缩略图 320、正文 800、全屏 1200),其余随意宽度的参数一律不暴露在 HTML 里。
  2. 在 CDN 侧配置“忽略特定 URL 参数”(Ignore Query String),如果不同参数只是微调锐化或细微尺寸,让边缘节点将其归一化处理。
  3. 如果必须保留动态处理能力,可以在图片子域名的 robots.txt 中封禁带特定参数 pattern 的爬取,或者在 HTTP 响应头中附带正确的图片级 Canonical:
Link: <https://www.llbbs.cn/uploads/cover.jpg>; rel="canonical"

通过这个响应头明确告知搜索引擎:所有带处理参数的衍生图片变体,其权威原始文件只有根目录下的那张 cover.jpg,爬虫不必把有限的抓取预算浪费在派生参数的探测上。

结构化数据 ImageObject 与页面实际渲染脱节的修正

全站推行现代图片改造后,还有一处容易遗漏的地方是 Schema.org 的结构化数据。

很多站长在改造前端组件时,把页面里的展示元素换成了 Picture,但后台生成的 JSON-LD 依然硬编码着旧的图片逻辑,甚至生成了早已失效或者重定向的失效路径。

Google 在搜索结果里展示富媒体卡片(Rich Snippets)时,会对 JSON-LD 中的 image 字段与页面实际加载呈现的主图进行交叉校验。如果 JSON-LD 里声明的图片无法正常下载,或者返回了破损的重定向,富媒体标记就会在站长平台上出现警告。

标准的 JSON-LD 声明应当显式提供稳定、可直接访问的标准格式 URL,避免把带动态参数或实验性格式的地址写入结构化数据:

{
  "@context": "https://schema.org",
  "@type": "TechArticle",
  "headline": "现代响应式图片改造中的 SEO 陷阱排查",
  "image": [
    "https://www.llbbs.cn/uploads/cover-16x9.jpg",
    "https://www.llbbs.cn/uploads/cover-4x3.jpg",
    "https://www.llbbs.cn/uploads/cover-1x1.jpg"
  ],
  "datePublished": "2026-10-26T01:00:00+08:00"
}

Google 推荐在结构化数据中提供 16:9、4:3 和 1:1 三种比例的基准图。这里的地址统一使用服务端能稳定回退的标准 JPEG 文件,不要把 image/avif 的内部协商 URL 塞进 JSON-LD。

前端跑分工具给出的建议是在理想实验室环境下的单点评估。把这些建议搬到线上生产环境时,必须考虑到各种爬虫客户端的解析能力差异和 CDN 边缘节点的缓存机制。每次动核心资源分发逻辑后,去翻翻抓取日志里的状态码和响应头,看看爬虫到底拿到了什么,往往比单纯盯 Lighthouse 跑分管用得多。

评论一下?

OωO
取消