7

网站图片无缝兼容 WebP/AVIF

 3 years ago
source link: https://www.cnblogs.com/index-html/p/img-webp-avif.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

网站图片无缝兼容 WebP/AVIF

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.webpfoo.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,那么显然会失败,从而导致业务损坏。

对于这个问题,有两种缓解方案:

  1. 判断 Fetch Metadata 相关的请求头,对于有能力读取文件数据的请求,则不考虑升级

  2. 通过黑白名单机制,只允许或不允许某些图片升级

第 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 显然无法运行,图片升级功能自然就失效了。这种情况只能使用后端方案。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK