V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
myqoo
V2EX  ›  分享创造

网站图片无缝兼容 WebP/AVIF

  •  1
     
  •   myqoo · 2021-07-21 11:05:06 +08:00 · 2238 次点击
    这是一个创建于 1224 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    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 规范即可。

    缺点 1

    由于同一个 URL 会返回不同的内容,如需通过 CDN 加速,则需配置 Vary: Accept 响应头,以确保代理服务器能根据不同的 Accept 请求头缓存相应的内容。然而目前 CloudFlare 免费版却无视 Vary,开启这个功能意味低版本浏览器显示不了图片!

    缺点 2

    不同格式的图片,即使像素完全相同,但文件数据显然是不同的。假如业务依赖文件数据,例如校验文件 Hash,那么显然会失败,从而导致业务损坏。

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

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

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

    第 1 种方案更通用,但 Fetch Metadata 只有较高版本的浏览器才支持,并且某些特殊场合仍可能存在问题。第 2 种方案更稳定,但需整理文件列表并在后端维护,显然很麻烦。

    前端方案

    如果网站搭建在虚拟空间、GitHub Pages 等这类无法修改配置的后端,或者使用了 CloudFlare 免费加速服务,那只能在前端实现。

    原理

    前端升级图片有多种方案。最容易想到的就是用 JS 在线解码高版本图片,但性能相比原生仍有差距,并且难以覆盖 CSS 中的图片。

    不过有一个黑科技使得前端升级图片变得非常容易,并能覆盖 CSS 等任何图片,那就是 Service Worker。它能拦截当前站点产生的所有请求,并能控制返回结果,相当于一个反向代理服务。于是可在 Service Worker 中判断 Accept 请求头,然后代理到相应的 URL 。

    实现

    得益于 Service Worker 强大的功能,图片除了格式升级外还能玩出很多「骚操作」,例如可将图片部署在免费的图床、相册上,使用时根据清单中的地址进行反向代理,从而可将图片流量降低到 0 !并且可准备多个图片 URL 做冗余备份,以及完整性校验等等。

    最近重新整理这个思路,并实现了一个工具: https://github.com/EtherDream/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 显然无法运行,图片升级功能自然就失效了。这种情况只能使用后端方案。

    6 条回复    2022-01-07 18:47:56 +08:00
    GTim
        1
    GTim  
       2021-07-22 08:08:10 +08:00
    问一个问题,后端的 avif 和 webp 或者 apng 是怎么生成的?
    myqoo
        2
    myqoo  
    OP
       2021-07-22 14:29:59 +08:00
    @GTim 写个脚本遍历所有图片文件,再调命令生成就可以。
    powerfj
        3
    powerfj  
       2021-07-23 10:28:14 +08:00
    赞一下, 思路不错.
    jingslunt
        4
    jingslunt  
       2021-07-23 15:46:48 +08:00
    AVIF 真的小
    no1xsyzy
        5
    no1xsyzy  
       2021-07-24 17:06:11 +08:00   ❤️ 1
    MrBrother
        6
    MrBrother  
       2022-01-07 18:47:56 +08:00 via iPhone
    @no1xsyzy

    感谢,试了一下后发现挺有用
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1880 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 16:29 · PVG 00:29 · LAX 08:29 · JFK 11:29
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.