WebP 格式发布已有十余年,但不少站点至今仍未使用,只为兼顾极少数低版本浏览器。至于去年发布的 AVIF 格式,使用的站点就更少了。
然而图片往往是流量大户,与其费尽心机优化脚本体积,可能还不如转换一张大图带来的收益更多。据 caniuse 统计,如今有 67% 的用户支持 AVIF、95% 的用户支持 WebP。先进的格式触手可得,却因兼容性问题仍坚守 PNG、GIF 等古老格式,白白浪费网站流量,以及用户加载时间,实在浪费。
事实上,对于同一个图片 URL,完全可为低版本浏览器使用老格式,为高版本浏览器使用新格式,从而实现无缝兼容。本文讲解后端和前端两种不同的实现方案。
这是最简单也是最普及的方案,网上能搜到很多相关的文章。不过这其中存在诸多细节,大多数文章都未考虑全面。
支持 WebP 的浏览器,HTTP 请求的 Accept
头会包含 image/webp
字符,后端可根据该特征返回 WebP 版图片;不支持的浏览器则没有该特征,后端返回原始图片。
AVIF 同理,特征为 image/avif
。由于 AVIF 比 WebP 更先进,因此需优先判断。
由于图片解码和编码开销很大,因此格式转换通常离线完成,例如预先将 foo.jpg
转换成 foo.jpg.webp
和 foo.jpg.avif
。这里有几个细节:
如果 WebP 文件比原文件更大,那就没有必要保留 WebP 文件。AVIF 同理
如果原文件本身就是 WebP 文件,那就不用再转 WebP 了。但可以尝试转 AVIF 版本,如果更小则保留
如果原文件本身就是 AVIF 文件,那什么都不用做
运行时的判断逻辑很简单,但也容易疏漏。以 nginx 为例,通常会这样配置:
http {
map $http_accept $_ext {
~image/avif .avif;
~image/webp .webp;
default '';
}
server {
location / {
add_header Vary Accept;
try_files $uri$_ext $uri =404;
}
}
}
看起来好像没问题,但遇到这种情况就不对了:用户支持 WebP 和 AVIF,但后端只存在 WebP 文件。正常应该返回 WebP,但这里 try_files
只尝试一次,找不到 $uri.avif 就返回原文件了。显然不对。
正确应该 try_files
两次:
http {
map $http_accept $_avif {
~image/avif .avif;
~image/webp .webp;
default '';
}
map $http_accept $_webp {
~image/webp .webp;
default '';
}
server {
location / {
add_header Vary Accept;
try_files $uri$_avif $uri$_webp $uri =404;
}
}
}
注意这里的逻辑顺序。即使用户不支持 AVIF 扩展名也不能直接返回空,否则就是在尝试原文件了。至于可能会重复尝试两次 WebP 文件,虽不优雅但也无大碍。
此外,如果希望用户访问目录名时 URL 末尾能自动添加 /
(例如访问 /blogs
时先重定向到 /blogs/
),那么 try_files
还需添加 $uri/
。
访问:https://www.etherdream.com/img-test/fox.html
支持 AVIF 的浏览器,返回的图片类型为 image/avif
不支持 AVIF 但支持 WebP 的浏览器,返回的图片类型为 image/webp
既不支持 AVIF 又不支持 WebP 的浏览器,返回的图片类型为 image/jpeg
后端实现的方案显然通用性很好,前端无需修改即可生效,甚至前端不是浏览器都没关系,只要遵循 HTTP 的 Accept 规范即可。
由于同一个 URL 会返回不同的内容,如需通过 CDN 加速,则需配置 Vary: Accept
响应头,以确保代理服务器能根据不同的 Accept
请求头缓存相应的内容。然而目前 CloudFlare 免费版却无视 Vary
,开启这个功能意味低版本浏览器显示不了图片!
不同格式的图片,即使像素完全相同,但文件数据显然是不同的。假如业务依赖文件数据,例如校验文件 Hash,那么显然会失败,从而导致业务损坏。
对于这个问题,有两种缓解方案:
判断 Fetch Metadata 相关的请求头,对于有能力读取文件数据的请求,则不考虑升级
通过黑白名单机制,只允许或不允许某些图片升级
第 1 种方案更通用,但 Fetch Metadata 只有较高版本的浏览器才支持,并且某些特殊场合仍可能存在问题。第 2 种方案更稳定,但需整理文件列表并在后端维护,显然很麻烦。
如果网站搭建在虚拟空间、GitHub Pages 等这类无法修改配置的后端,或者使用了 CloudFlare 免费加速服务,那只能在前端实现。
前端升级图片有多种方案。最容易想到的就是用 JS 在线解码高版本图片。当初 WebP 发布时我对此颇有兴趣,尝试用 Flash 实现 WebP 解码器,并且能自动替换网页中的图片元素,看起来就像原生支持一样。但实际应用后发现并不理想,一是不支持 CSS 图片(实现很麻烦),二是解码性能差。虽然使用了 Alchemy 编译技术(LLVM → ActionScript ByteCode),但性能相比原生仍差一大截。最终放弃了这个方案。
尽管后来有更先进的计算方案,例如 WebWorker、asm.js / WebAssembly、SIMD 等,但仍然达不到原生性能,并且代码体积很大。所以在线解码的方案仍不考虑。
直到另一个黑科技的出现,使得前端升级图片变得非常容易,并能覆盖网页中所有图片,那就是 Service Worker
。它能拦截当前站点产生的所有请求,并能控制返回结果,相当于一个反向代理服务。于是我们可以在 Service Worker 中判断 Accept
请求头,然后代理到相应的 URL。
得益于 Service Worker 强大的功能,图片除了格式升级外还能玩出很多「骚操作」,例如可将图片部署在免费的图床、相册上,使用时根据清单中的地址进行反向代理,从而可将图片流量降低到 0!并且可准备多个图片 URL 做冗余备份,以及完整性校验等等。这个思路之前在 网站 CDN 去中心化 尝试过,不过实际应用起来似乎并不容易。
最近我重新整理这个思路,并实现了一个工具:freecdn,它可以自动生成清单文件,记录原文件的备用 URL 列表、Hash 值、是否支持 WebP/AVIF 升级等信息。
访问:https://freecdn.etherdream.com/fox.html
Service Worker 不仅将 JPEG 升级成 AVIF 版本,甚至从免费 CDN 加载,将流量开销「优化」到了零!
还有更有趣的现象 —— 新建一个隐身窗口,打开控制台网络栏,访问:https://freecdn.etherdream.com/fox.jpg
从浏览器界面上看,和直接访问图片一模一样,但实际上该图片是由 Service Worker 提供的。具体原理和细节可 查看这里。
后端无需任何修改,无论是普通的服务器、CDN 还是虚拟空间都可以。前端只需引用一个脚本开启 Service Worker,无需修改业务逻辑。
由于 Service Worker 运行在前端,因此能获取到更详细的 请求上下文信息,从而可实现更智能的策略。此外,即使要配置黑白名单,只需通过一个清单文件即可实现,比修改后端服务配置方便很多。
如果用户的浏览器不支持脚本,或者根本不是浏览器访问,那么 Service Worker 显然无法运行,图片升级功能自然就失效了。这种情况只能使用后端方案。