升级到AVIF之咕咕咕

开头瞎扯

好久不见,这里是鸽子王小霖。一转眼就过了两年,小霖也从辣鸡高中升到了辣鸡带学。高三在学校里的时候一直在想暑假要干嘛干嘛,结果一到暑假就开始”一蹶不振”,一直摆到现在。

上次动这个博客是2020年10月,当时应该是更改了博客的主题并且稍稍优化了一下,但还留下了一些小细节没有修,于是最近忙里偷闲稍稍修了一下,顺便把图片换成了AVIF,然后顺便来水篇文章。(标题催更隔壁辰叔叔,绝对不是懒得想

AVIF

AVIF是Netflix整出来的图片格式,它的初衷是为了寻找比 JPEG具有更好压缩率、更多功能特性的图片格式。下面是从借鉴的表格。

格式透明背景动图有损压缩的体积(同图景)兼容性
PNG支持有 aPNG 格式广泛支持
JPEG不支持不支持比 PNG 小广泛支持
WebP支持支持比 JPEG 小Apple 很晚才支持,动图效果差
HEIC支持支持比 WebP 小Windows 上要另外安装 HEVC 视频编码解码器
AVIF支持支持比 WebP, HEIC 小太新,Apple 各平台还未支持,Windows 要安装 AV1视频编码解码器

至于性能表现什么的这里就不说那么多了,毕竟网上一抓一大把,Netflix原始发布AVIF的地方在这里,如果有兴趣可以去读读

所以总而言之AVIF就是个各个方面吊锤PNG和WebP的文件格式啦,所以换成AVIF只能说是大势所趋了

但是当我们一打开caniuse,这一片红…实在是太红了…

image-20220518205920502

对比一下,下面这个是WebP的支持情况

image-20220518163420891

但是挖的坑总不能就这样不填了吧,活还是要继续整的

检测可用性

检测WebP在浏览器是否可用的方法已经比较多了,主流就是通过创建一个空画布并且通过toDataURL方法来转换画布为base64编码的WebP图像,如果浏览器支持就会正常返回一个data:image/webp;base64开头的base64字符串,但是截止现在浏览器还不能返回data:image/avif;base64的字符串,所以我们就得另找办法了

当浏览器通过<img>标签加载到一张自己不知道格式的图片时,就会触发error事件,所以我们只要提供一个base64编码过的AVIF格式图像给浏览器加载,并且检查是否有错误的产生就可以了,所以代码就像下面这样

Update:如评论中所说,toDataURL检测WebP的方法不是在所有浏览器都生效,因此全部都换成了加载图片的检测方法

function getImageCapbility() {
  if (__data('imageType')) {
    return Promise.resolve(__data('imageType'))
  }

  return new Promise((resolve, reject) => {
    var image = new Image()
    image.onerror = reject
    image.onload = resolve
    image.src =
      'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgANogQEAwgMg8f8D///8WfhwB8+ErK42A='
  }).then(
    () => {
      __data('imageType', 'avif')
      return 'avif'
    },
    () =>
      new Promise((resolve, reject) => {
        var image = new Image()
        image.onerror = reject
        image.onload = resolve
        image.src =
          'data:image/webp;base64,UklGRjIAAABXRUJQVlA4ICYAAACyAgCdASoCAAEALmk0mk0iIiIiIgBoSygABc6zbAAA/v56QAAAAA=='
      }).then(
        () => {
          __data('imageType', 'webp')
          return 'webp'
        },
        () => {
          __data('imageType', 'png')
          return 'png'
        }
      )
  )
}

但是这样做的缺点也是很显而易见的,就是会在Network中多出一个丑丑的请求

image-20220518212732998

为了避免这玩意儿太污染我们的时间线,上面的代码就做了一个小小的缓存,缓存助手函数的代码如下

const __data = (function dataHelperHelper() {
    let data = {}

    if (localStorage && localStorage.getItem('lyn_data')) {
      data = JSON.parse(localStorage.getItem('lyn_data'))
    }

    return function dataHelper(key, value) {
      if (value === undefined) return data[key]
      data[key] = value
      localStorage && localStorage.setItem('lyn_data', JSON.stringify(data))
    }
  })()

动态替换图片格式

在一开始其实小霖有几种想法,第一种是通过阿里CDN的EdgeRoutine,一个和Cloudflare Workers很类似的东西,在CDN边缘节点进行动态替换,虽然小霖之前内测薅到了资格,但现在好像还是商业化了,而且按天计费,一天最少1块钱的样子,再加上还有一些万年BUG没有修,所以就扔一旁去了

其实除了钱还有另外一个问题,就是accept头的问题。在你通过脚本发起请求的时候,这个头浏览器是不会去自动填充的,也就是说只有通过img script link这类标签加载的资源浏览器才会将真实的符合上下文的accept头发送出去,也就是说如果通过pjax进行页面切换,服务器是拿不到用户真实的accept头的

其实还有几种怪方法,就全部写在下面了

  • 向一个不带后缀的图片地址发起请求,由边缘Worker决定返回哪种类型的图片
    • 但是阿里这东西不保流量证一定会经过Worker,也就是可能会出现Worker出问题了无法捕捉到流量的情况,而又使用类似/dist/1.png这样固定类型的后缀来返回动态类型的图片,所以🤷🏻‍♀️
  • img标签全部换成picture,用source来写明每一种类型图片的地址
    • 丑拒..

所以到最后我们可能只能够通过上面一节的内容在用户端动态更改图片链接了,哪怕这可能不太符合Best practice

因为首屏是由浏览器直接从HTML文件渲染的,而img标签又没有beforeLoad这种事件,所以我们只能尽快的替换掉img标签里的src属性,但是浏览器的玄学调度也不能保证我们的代码一定在图片加载前执行,所以也有可能会出现下面这种请求canceled的情况,如下图

image-20220519164449134

对于首屏来说,除了在服务端下手脚应该没有别的方法来解决了,但是对于第二次打开,我们还是有办法解决的,也就是通过ServiceWork直接更改浏览器获取到的HTML数据

现在小霖的博客应该是已经用上了这个小hack了,大家可以打开DevTool康康首屏的请求是不是变成AVIF了

结尾胡扯

嘛这篇文章大概就这些内容了,上了大学又摆又忙,不过还是做了不少奇奇怪怪有趣的东西的,后面有空可以再来水水博文

就这样吧,大家晚安捏捏捏

本文采用 CC BY-NC-SA 4.0 许可协议。转载和引用时请注意遵守协议、注明出处!