WebP 配完之后:为什么同一张图有时 .jpg 有时 .jpg.webp

前言:上篇解决了"怎么配",这篇解决"配完之后的怪事"

上一篇记录了如何用 Apache 重写规则,让全站图片自动上 WebP,文章正文一个字不改。

规则配完、验证通过后,本来以为收工了。结果过了几天在实际浏览时发现一个说不通的现象:

  • 用无痕窗口打开某篇文章,图片链接是 photo.jpg
  • 按 Ctrl+F5 硬刷新之后,同一张图的链接变成了 photo.jpg.webp

同一个页面、同一张图,两种结果。

更让人困惑的是:后台缓存清了不下十次,现象依旧。当时第一反应是怀疑 .htaccess 规则写错了,或者是缓存插件没清干净。

但这两个猜测都是错的。真正的答案藏在"谁动了我的 HTML"这个问题里。本文完整记录排查过程、根因分析、解决方案,以及一套可以直接复用的验证脚本。

适用环境:WordPress + Apache + cwebp 预转换 + 页面缓存插件(本文以 Cache Enabler 为例,其他带 WebP 选项的缓存插件同理)。

一、先排除一个错误直觉:不是 .htaccess 干的

排查的第一步,是先确认我们自己的规则不可能造成这种现象。

回顾一下 Apache 重写方案的工作原理:

  1. 浏览器请求 demo.jpg,请求头带 Accept: image/webp
  2. Apache 在服务器内部把要读取的文件换成 demo.jpg.webp
  3. 响应头改成 Content-Type: image/webp

这里有个关键点:

内部重写不改变 URL。

浏览器地址栏、HTML 源码、Network 面板里看到的名称,永远都是 demo.jpg。后缀变化只发生在浏览器看不到的服务器内部。

所以——只要你在 HTML 源码里看到了 .jpg.webp 这个后缀,就百分之百可以断定:有人在 HTML 层面把链接字符串改写了,跟重写规则没有任何关系。

这一条判断,是整个排查的突破口。

二、关键实验:让 curl 模拟浏览器

既然现象跟"是否无痕、是否硬刷新"有关,那很可能跟请求头有关。用 curl 直接验证。

第一次测试(不带头,血的教训)

curl -s https://www.example.com/demo-article.html | findstr /i "photo"

结果:

<img src="https://www.example.com/static/photo.jpg" alt="..." />

看到 .jpg,差点就下结论"HTML 是干净的,问题在别处"。

但这个测试是错的。

为什么第一次的结论不成立

curl 默认发送的请求头是:

Accept: */*

它不包含 image/webp。而真实浏览器发的是:

Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8

缓存插件就是靠这个头判断"该给访客哪一份 HTML"。curl 没带这个头,拿到的是非 WebP 版的那份缓存。

第二次测试(带上 Accept,真相浮现)

curl -s -H "Accept: text/html,application/xhtml+xml,image/webp,*/*" ^
     https://www.example.com/demo-article.html | findstr /i "photo"

结果:

<img src="https://www.example.com/static/photo.jpg.webp" alt="..." />

同一篇文章,两种不同的 HTML。实锤了。

这里要记住一个通用经验:用 curl 测试任何跟内容协商有关的机制时,必须手工补上 Accept 头,否则你测的根本不是浏览器看到的东西。这个坑在测图片、测压缩、测 WebP 时都会踩到。

三、两套机制并存:它们在做同一件事

现在答案清楚了:站点上其实有两套 WebP 方案在同时运行。

对比项 Apache 内部重写 缓存插件改写 URL
实现方式 服务器内部换文件 在 HTML 里改 src 字符串
HTML 里的 URL 始终是 .jpg,不变 变成 .jpg.webp
覆盖范围 所有图片请求,含 CSS 背景图、JS 里的图 只能改 HTML 里的 <img> 标签
文件不存在时 自动回退原图(有 -f 判断) 直接请求 .jpg.webp → 404
HTML 缓存份数 1 份 2 份(普通版 + WebP 版)

两者最终都让浏览器拿到了 WebP 图,但路径完全不同。

四、为什么清了十几次缓存都没用

这是整个排查中最反直觉的一点。

因为缓存插件本来就该生成两份。它的目录结构大致是这样:

wp-content/cache/cache-enabler/
    └── 站点目录/
        ├── demo-article.html          ← 给不支持 WebP 的浏览器
        └── demo-article-webp.html     ← 给支持 WebP 的浏览器

你清一次缓存,它重新生成两份;再清一次,还是两份。

所以"清缓存没用"根本不是缓存没清干净,而是——这个现象就是它的正常工作方式。你在无痕、硬刷新之间看到的差异,只是不同时机命中了不同副本而已。

经验教训:当你反复执行某个操作却总是得到同样结果时,别急着加大力度,先停下来想一想——是不是这个"异常"本来就是设计如此。

五、真正的风险:无脑追加后缀

两套并存时功能上其实没坏:浏览器请求 photo.jpg.webp,重写规则因为后缀不是 jpg/png 所以不匹配,直接把文件返回了。能正常显示,只是绕了一圈。

但这里埋着一个雷。

缓存插件改写 URL 的方式是追加 .webp 后缀,它不会去检查这个文件是否真的存在。回想一下上一篇的转换过程:

  • 全站原图 1500 张
  • 实际转换成功 1149 个 WebP

那剩下的三百多张(转换超时、文件损坏、新上传还没转),HTML 里照样会被改成 .jpg.webp。

结果就是:这些图片 404,永久裂图。

而重写规则里有这么一条兜底:

RewriteCond %{REQUEST_FILENAME}.webp -f

含义是"只有当 .webp 文件确实存在时才替换",不存在就自动返回原图。这就是两套方案最本质的差距:一个靠运气,一个有兜底。

六、解决方案:把改写权收回来

思路很简单——关掉缓存插件的 WebP 改写,只保留 Apache 重写这一套。

操作步骤

  1. WordPress 后台 → 设置 → Cache Enabler
  2. 在「缓存行为」区块找到这项:
    ☑ 创建一个缓存版本以支持 WebP。使用 Optimus 将您的图像转换为 WebP。
  3. 取消勾选
  4. 点击底部的「保存更改并清除站点缓存」(用这个按钮,一次搞定保存和清缓存)

关于选项文案的坑

这个选项是德文插件汉化过来的,措辞相当绕——"创建一个缓存版本以支持 WebP",完全不像个开关名。很多人翻遍设置页都找不到,就是因为它在描述"做什么"而不是"开关叫什么"。

另外注意后半句提到的 Optimus:这说明插件自己不转换图片,它是配合 Optimus 插件使用的。如果你像本文一样用 cwebp 自己转换,那这个选项对你只有 URL 改写这一个作用,而这件事重写规则做得更好。关掉它没有任何损失。

如果插件版本较老,没有这个选项

那就退而求其次——把缺失的 WebP 补齐,消除裂图隐患。先查出哪些图没有对应 WebP:

cd /d D:\webroot
for /r %i in (*.jpg) do @if not exist "%~i.webp" echo %i

把列出的文件补转一遍即可。两套机制并存的"绕一圈"问题依然存在,但至少不会裂图。

七、验证:七项全绿才算收工

把下面的内容保存成 webp-check.bat,双击运行即可。注意里面用的是 curl.exe,原因见本节末尾。

@echo off
chcp 65001 >nul
set REF=Referer: https://www.example.com/
set WEBP=Accept: image/webp,*/*

echo ============ WebP 最终验证 ============
echo.
echo [1] 图片目录(应为 image/webp)
curl.exe -s -D - -o nul -H "%REF%" -H "%WEBP%" ^
  https://www.example.com/static/photo.jpg | findstr /i "content-type"
echo.
echo [2] 第二个图片目录(应为 image/webp)
curl.exe -s -D - -o nul -H "%REF%" -H "%WEBP%" ^
  https://www.example.com/img/demo.jpg | findstr /i "content-type"
echo.
echo [3] uploads 目录(应为 image/webp)
curl.exe -s -D - -o nul -H "%REF%" -H "%WEBP%" ^
  https://www.example.com/wp-content/uploads/2017/09/logo.png | findstr /i "content-type"
echo.
echo [4] 降级测试(应为 image/jpeg)
curl.exe -s -D - -o nul -H "%REF%" -H "Accept: image/jpeg,*/*" ^
  https://www.example.com/img/demo.jpg | findstr /i "content-type"
echo.
echo [5] HTML 里的 URL(应为 .jpg,不带 .webp)
curl.exe -s -H "Accept: text/html,image/webp,*/*" ^
  https://www.example.com/demo-article.html | findstr /i "photo"
echo.
echo [6] 缓存目录里的 webp 副本(应为空白)
dir /s /b D:\webroot\wp-content\cache\cache-enabler\*-webp.html 2>nul
echo.
echo ============ 验证结束 ============
echo 1-3 应为 image/webp,4 为 image/jpeg,5 无 .webp,6 无输出
pause

判定标准

项 期望结果 说明
1~3 Content-Type: image/webp 三个目录的重写都生效
4 image/jpeg 不支持 WebP 的浏览器正常拿原图
5 URL 是 .jpg,不带 .webp 插件改写已关闭
6 无任何输出 WebP 副本已清空,缓存体积减半

为什么脚本里写 curl.exe 而不是 curl

这是 Windows 上最常见的翻车点。在 PowerShell 里,curl 是 Invoke-WebRequest 的别名,不是真正的 curl。直接敲:

curl -s -D - -o nul ...

会报一堆莫名其妙的错,比如"缺少参数 SessionVariable"——因为它把 -s 当成了 PowerShell 的参数。

解决办法就是写全 curl.exe,加后缀后绕过别名,CMD 和 PowerShell 里都能正常执行。

另一个常见错误:把上一条命令的输出一起粘进了命令行,导致报"此时不应有 <"。批处理脚本能彻底避免这类问题,这也是建议写成 .bat 的原因。

八、WebP 到底有什么好处(诚实版)

折腾了这么久,值得吗?分三个层面说,不夸大。

① 省带宽——收益最大的一项

如果你的站点没有用 CDN,所有图片流量都走自家服务器,那么这一项收益是直接折算成钱的。

一个访客打开一篇带 15 张配图的文章:

原图 WebP 传输量 约 3.5 MB 约 2 MB 节省 约 43%

传输量减半,意味着同样带宽能服务的访客数翻倍,服务器并发压力也同步下降。对一个有上千篇文章的站点,一个月省下的流量相当可观。

② 打开更快

图片通常占页面总重量的 60%~75%。图片小一半,页面加载时间大致也砍半。移动端走 4G/5G、信号不稳时,这个差异尤其明显。

③ 对 SEO:有用,但别指望它翻盘

这里必须说清楚,避免误导:

  • Google:页面速度是明确的排名因素,体现在 Core Web Vitals 的 LCP(最大内容绘制)指标上。图片往往是 LCP 元素,图片小了 LCP 自然改善。
  • 百度:有闪电算法,移动端首屏加载速度影响排名权重。

但它的权重很低。搜索排名的决定性因素排序大致是:

内容质量 + 外链权威  >>>  页面速度  >>>  是否用 WebP

内容不行、外链没有,用不用 WebP 都排不上去;内容好的前提下,WebP 能带来的改善大概在百分之几的边际量级。

所以正确的期待是:主要收益在用户体验和带宽成本,排名提升是附带的。

什么情况下不划算

情况 说明 已用 CDN 多数 CDN 自带 WebP 转换,自己再做一遍是重复劳动 站点图片很少 收益不明显,折腾成本大于收益 服务器 CPU 很弱 如果是实时转换(每次请求现转)会拖慢服务器——但本文方案是预先转换,零实时开销

九、自己量出节省比例

别信估的百分比,实测最准。在 PowerShell 里跑:

$p = "D:\webroot\wp-content\uploads"
$jpg  = (Get-ChildItem $p -Recurse -Include *.jpg,*.png | Measure-Object -Sum Length).Sum
$webp = (Get-ChildItem $p -Recurse -Include *.webp      | Measure-Object -Sum Length).Sum
"原图总计: {0:N1} MB" -f ($jpg/1MB)
"WebP总计: {0:N1} MB" -f ($webp/1MB)
"节省比例: {0:P1}"    -f (1 - $webp/$jpg)

把 $p 换成其他图片目录再跑一次。PNG 多的目录节省比例通常明显高于 JPG 目录,因为 PNG 转 WebP 的压缩空间更大(PNG 一般能小 50%~70%,JPG 只有 25%~35%)。

用 DevTools 亲眼确认

F12 打开开发者工具 → Network 面板 → 右键表头勾选「类型」列 → 刷新页面,看图片请求:

名称 状态 类型 photo.jpg 200 webp

名称写着 .jpg,类型却是 webp——这就是内部重写最直观的证明。点开该请求看 Response Headers,能看到 content-type: image/webp。

十、最终形态

调整完成后,整个机制变成干净的单线条:

文章 HTML:<img src="xxx.jpg">        ← 上千篇文章一个字没改
     ↓
浏览器请求 xxx.jpg,带 Accept: image/webp
     ↓
Apache 检查 xxx.jpg.webp 是否存在
     存在   → 返回 WebP,响应头标 image/webp
     不存在 → 返回原图                    ← 自动降级,永不裂图
     ↓
同一 URL 按 Accept 分别缓存(Vary: Accept)

同时拿到四个好处

项 调整前 调整后
裂图风险 有(无脑追加 .webp) 无(-f 判断兜底)
HTML 缓存体积 两份并存 减半
覆盖范围 只改 HTML 的 <img> 所有图片,含 CSS 背景图
正文链接 被插件改写 原样保留

写在最后:两个通用经验

这次排查真正的价值不在"关掉一个选项",而在两条可以复用的方法论:

第一,先划清责任边界,再动手找原因。一开始怀疑 .htaccess 写错了,但其实只要记住"内部重写不改 URL"这一条铁律,就能在一分钟内排除这个可能,直接锁定真凶。很多时候排查慢,不是因为技术难,而是因为没有先把"谁不可能"排除掉。

第二,测试工具本身可能是最大的干扰源。第一次 curl 测试给出了完全错误的结论,原因仅仅是它默认不带 Accept 头。用工具验证浏览器行为时,先问一句:这个工具的默认行为,跟浏览器一样吗?

这两条经验,在排查压缩、缓存、内容协商、防盗链等所有跟"请求头"相关的问题时,都同样适用。


发布日期:

所属分类: 网站运营 SEO 标签:  



下一篇:

没有了,已经是最新文章