各位好久不见!
过去的时间中我们一直在忙着推进 WebP Cloud 和 Public Services 内部基础设施的迭代和优化,少有文章发布,甚至一度有些朋友以为我们产品已经“凉了” 😆,好在我们没有,此外,现在终于有时间来更新一下 WebP Server Go 的进展了。
我们相信开源软件是自由的根基,我们在努力推进 SaaS 产品 WebP Cloud 和 Public Services 的同时,也一直在维护和改进开源版本,让用户始终拥有更多的选择。
最近我们发布了 WebP Server Go 0.16.0。这个版本看上去只是在浏览器识别和响应头上做了一些调整,但它解决了一个伴随项目很久的问题:同一张图片应该如何可靠地输出为浏览器真正支持的格式,以及这些不同格式的响应应该如何被 CDN 正确缓存。
WebP Server Go 是什么
WebP Server Go 是一个采用 GPLv3 协议、代码托管在 https://github.com/webp-sh/webp_server_go 上的开源图片优化程序。
它可以在不修改原始图片 URL 的情况下,动态(On the fly)将 JPG、PNG 等图片优化为 WebP、AVIF 或 JPEG XL,并从浏览器支持的格式中选择体积更小的版本输出。
例如,网页中的图片地址依然可以是:
https://example.com/pics/photo.jpg
但是支持 AVIF 的浏览器可能收到 image/avif,支持 WebP 的浏览器可能收到 image/webp,不支持这些现代格式的客户端则进入原图回退分支。整个过程不需要提前批量处理图片,也不需要修改网页中的图片地址。
为什么我们要重新设计浏览器能力判断
过去 WebP Server Go 会同时参考 Accept 请求头和 User-Agent,尝试从浏览器名称、操作系统与版本号推测它支持 WebP、AVIF 还是 JPEG XL。
这种方式在早期可以覆盖一些浏览器没有完整发送 Accept 的场景,但随着浏览器版本和图片格式支持不断变化,User-Agent 推测也越来越容易出现偏差。
JPEG XL 是一个很典型的例子。Chrome 曾经移除实验性支持,Safari 随后正式支持,而最近 Mozilla 和 Chromium 又分别推进了默认启用 JPEG XL 的工作。如果继续维护一套“某个浏览器从某个版本开始支持某种格式”的规则,我们就需要不断追赶浏览器的发布节奏,而且一旦判断错误,就可能向客户端发送一张它无法解码的图片。
事实上 HTTP 已经提供了专门解决这个问题的机制:客户端通过 Accept 明确告诉服务器自己能接受什么,服务器再通过 Content-Type 告诉客户端最终返回了什么。
所以从 0.16.0 开始,我们删除了基于 User-Agent 的图片格式推测,统一使用 Accept 请求头进行内容协商。
更严格地解析 Accept
移除 User-Agent 判断并不意味着简单搜索一下请求头中有没有 image/webp、image/avif 或 image/jxl 就结束了,因为 Accept 还包含质量值(q value)。
例如下面这个请求:
Accept: image/webp,image/jxl;q=0
其中 image/jxl;q=0 的意思不是“支持 JPEG XL 但优先级最低”,而是“JPEG XL 对这个请求不可接受”。根据 RFC 9110 的定义,质量值为 0 的内容会被认为不可接受。
旧版本只要看到字符串中包含 image/jxl 就可能把它判断为支持(我们被骗啦!)。
if strings.Contains(accept, "image/jxl") {
supported["jxl"] = true
}
而 0.16.0 会解析完整的媒体类型和质量值,仅在格式被明确列出且 q>0 时启用对应的现代图片格式。
新的判断规则可以概括为:
image/webp:支持 WebPimage/avif;q=0.8:支持 AVIFimage/jxl;q=0:不支持 JPEG XLimage/*或*/*:不据此推断支持任何现代图片格式- 缺少或无法解析的
Accept:进入原图回退分支
我们有意不把通配符当作 WebP、AVIF 或 JPEG XL 支持信号。通配符在语义上可能接受许多内容,但它并不能证明客户端真的具备对应格式的解码能力;在不确定的时候返回原图,比乐观地发送一张无法显示的图片更安全。
例如,假设服务器启用了 AVIF/WebP 转换,并且转换后的 AVIF 体积最小:
curl -I \
-H 'Accept: image/avif,image/webp,image/*;q=0.8' \
http://127.0.0.1:3333/DSC05955.jpg
响应中会看到类似:
HTTP/1.1 200 OK
Content-Type: image/avif
Vary: Accept
X-Compression-Rate: 0.13
如果改成明确拒绝 JPEG XL:
curl -I \
-H 'Accept: image/jxl;q=0,image/webp' \
http://127.0.0.1:3333/DSC05955.jpg
WebP Server Go 就不会返回 image/jxl。
Vary: Accept 与 CDN 缓存
内容协商解决了源站应该输出什么格式的问题,但前面如果还有一层 CDN,就会遇到另一个问题。
对于 CDN 来说,下面两个请求访问的是同一个 URL:
GET /pics/photo.jpg
Accept: image/avif,image/webp
GET /pics/photo.jpg
Accept: image/jpeg,image/png
如果 CDN 只使用 URL 作为缓存键,那么第一个访客触发并缓存的 AVIF 图片,就有可能被直接发送给后面不支持 AVIF 的访客。过去由于 WebP Server Go 没有完整表达这个响应差异,我们在文档中一直建议用户禁用 CDN 图片缓存,这虽然安全,但也放弃了 CDN 最重要的性能优势。
从 0.16.0 开始,我们决定解决这个问题,所有经过图片格式协商的响应都会带上:
Vary: Accept
这个响应头告诉共享缓存:同一个 URL 的响应会随着请求中的 Accept 而变化,不能把一个格式的缓存直接用于另一种能力不同的客户端。
不过需要注意,源站返回 Vary: Accept 并不代表所有 CDN 都会自动按它拆分缓存:
- Cloudflare 需要在 Cache Rules 中启用 Vary,并建议将
Accept归一化为image/avif、image/webp和image/jxl等实际使用的能力 - Fastly 会按照 HTTP 规范处理
Vary,但建议对Accept做归一化,避免等价请求产生过多缓存条目 - Amazon CloudFront 需要在 Cache Policy 中把
Accept加入缓存键 - 不支持
Vary或自定义缓存键的 CDN,仍然应该对 WebP Server Go 的图片路径禁用缓存
我们已经重写了相关文档,具体配置与验证方法可以参考:WebP Server Go 与 CDN 配合使用。
如果你使用了 width、height、max_width 或 max_height 等图片处理参数,也需要确保这些查询参数仍然包含在 CDN 缓存键中。启用新的变体缓存规则后,别忘了清理此前已经生成的旧缓存。
对现有用户有什么影响
对于正常通过 <img>、CSS 或其他图片上下文加载资源的现代浏览器,它们通常会在 Accept 中明确声明支持的图片格式,因此升级后不需要修改网页代码。
行为变化主要出现在以下场景:
- 过去仅依赖 User-Agent 被识别为支持现代格式,但请求中没有明确声明对应格式
- 使用只发送
Accept: */*的脚本、爬虫或命令行客户端 - 自建反向代理或 CDN 在转发时删除或重写了浏览器的
Accept请求头
这些请求在 0.16.0 中会进入更保守的原图回退分支。如果你使用 cURL 或程序化客户端测试某种格式,请显式发送对应的 Accept,例如:
curl -I -H 'Accept: image/webp' https://example.com/pics/photo.jpg
这次升级没有新增必须修改的配置项,现有的 CONVERT_TYPES、质量设置和图片路径配置可以继续使用。同时,由于不再解析 User-Agent,我们也移除了 github.com/mileusna/useragent 依赖,让格式协商逻辑变得更简单、更容易测试。
如何升级
如果你使用 Docker Compose 部署,可以执行:
docker compose pull
docker compose up -d
升级后建议用不同的 Accept 请求头检查同一张图片,确认响应中包含 Vary: Accept,并根据你使用的 CDN 配置对应的缓存规则。
0.16.0 并没有加入一个醒目的图片处理功能,但它让 WebP Server Go 回到了更清晰的 HTTP 语义上:浏览器明确声明能力,源站选择输出格式,缓存也能够知道响应为什么不同。随着 JPEG XL 等格式逐渐进入更多浏览器,这套机制也比持续维护浏览器版本表更能适应未来的变化。
希望大家能喜欢这个版本。如果你在升级或配置 CDN 时遇到了问题,或者有什么新的想法,欢迎前往 Issues · webp-sh/webp_server_go 提交 Issue 反馈!