findxc / blog

88 stars 5 forks source link

关于 WebP 格式的图片 #64

Open findxc opened 2 years ago

findxc commented 2 years ago

简介

虽然 WebP 格式的图片已经出来很久很久了,但是最近才仔细研究了下,恩,后知后觉 =.=

浏览器兼容性如下,还是挺不错的了。

A4BB4350-5C74-46A8-941A-A68331AD92C4

很多网站都已经用上了,比如下面这些:

简单来说,WebP 格式的图片体积小并且足够清晰。

可以去这里实际感受一下:WebP - 图片格式的发展趋势 - 又拍云

怎么用

那在实际开发过程中,我们怎么把这种格式图片使用起来呢?

如果你不需要兼容 IE ,也不考虑低版本的浏览器不支持 WebP 的场景,那你直接就使用 .webp 的图片就行了,就像你使用 .png 那样。

如果你要考虑兼容,并且图片是放在 CDN 上,看看 CDN 有没有提供自动返回 WebP 格式的功能,比如又拍云是支持的,它的实现原理文档也有写:如何设置 WebP 自适应? – 又拍云-文档帮助中心

大概原理就是请求头的 accept 如果包含 webp ,就返回 WebP 格式图片,否则返回原本图片,这样实现了兼容。

如果你的图片没放 CDN ,如果你对 nginx 熟悉,那你可以考虑在 nginx 层面根据请求头的 accept 做不同处理,如果包含 webp ,并且还没有生成 WebP 格式图片,就生成并返回,如果不包含 webp 就直接返回原图。

相当于把 CDN 的自适应放在 nginx 这一层去做。这样不用改动业务代码,会比较省事。

网上有实现,因为我对 nginx 没那么熟我就放弃这种做法了。

我采用的一种相对风险较低的做法,就是 nginx 只负责返回 WebP 格式图片或者原图,WebP 格式图片直接维护在项目中,这样就省掉了 nginx 生成这一步。下面来看具体做了哪些事。

具体需要做哪些处理

nginx 相关配置

代码见 https://github.com/findxc/react-hello/tree/v0.0.2/nginx

nginx.conf 的 http 配置中需要补充以下代码:

# 使用 map 语法定义 webp_suffix 变量,默认值为空字符串
# 如果请求头的 accept 值包含 webp ,就把 webp_suffix 的值设为 .webp
map $http_accept $webp_suffix {
    # ~* 表示大小写不敏感, ~ 表示大小写敏感
    ~*webp .webp;
}

然后在 default.conf 的 server 中补充以下代码:

# 不区分大小写匹配所有带 hash 的图片,比如 xxx.[hash].png
location ~* ^(.+)\.[0-9a-z]+\.(png|jpg|jpeg|gif)$ {
    set $base $1;
    set $origin_type $2;
    set $webp_uri $base.$origin_type$webp_suffix;
    # try_files 会先去看 $webp_uri 有没有,没有再去找 $uri
    # 这个 404 是必须的,否则会造成死循环,详见 https://blogs.vicsdf.com/article/762
    try_files $webp_uri $uri =404;

    add_header Vary Accept;
    add_header Cache-Control max-age=31536000;
}

# 因为 location 会优先匹配上面的,所以下面这个是针对没有 hash 的图片,比如直接 xxx.png
location ~* \.(png|jpg|jpeg|gif)$ {
    set $webp_uri $uri$webp_suffix;
    try_files $webp_uri $uri =404;

    add_header Vary Accept;
    add_header Cache-Control max-age=0;
}

以上配置实现的功能就是,当客户端请求 xxx.hash.png 或者 xxx.png 时,nginx 会先去看有没有 xxx.png.webp ,有的话就返回,没有就返回原图。

业务代码中的图片打包一般会带有 hash ,而 public 中的图片打包后不会有 hash ,所以这里考虑了两种情况。

还有,为什么 xxx.hash.png 也是去找 xxx.png.webp 呢?下面说。

打包代码时没打包 .webp

因为实际代码中 import 的还是 xxx.png ,那就不会打包 .webp ,那咋整呢?

就需要改一下 webpack 配置了,如下:

const CopyPlugin = require('copy-webpack-plugin')

// 给 webpack 配置增加一个 plugin
plugins: [
  ...(process.env.NODE_ENV === 'production'
    ? [
        new CopyPlugin({
          patterns: [
            // 这里的 to 根据项目实际打包后静态资源路径来
            { from: 'src/**/*.webp', to: 'static/media/[name].[ext]' },
          ],
        }),
      ]
    : []),
],

这样打包后就有 .png 也有 .webp 啦。由于这里只是单纯 copy ,所以生成的 .webp 是没有 hash 的。这也是为什么上面说 xxx.hash.png 也是尝试去找 xxx.png.webp 。

ok,到这里你去实际打包,nginx 配置改了重启一下,就能看到界面上显示的是 WebP 格式图片啦,虽然请求的路径还是 xxx.png ,但是从控制台看文件的类型确实是 webp ,并且体积也是 webp 对应的大小。

采用这种方式的话,对业务代码的改动也比较小。

因为突然想到还有一种使用 WebP 的方式,就是在前端判断浏览器是否支持 WebP 然后使用不同图片,但是这种对业务代码改动就比较大了。

怎么为历史项目的图片生成 .webp

我是用的这个工具:Downloading and Installing WebP  |  Google Developers

在 macOS 上可以使用 brew 去安装。

brew update
brew upgrade
brew install webp

这里记得更新下 brew ,这样才能安装最新版的 webp ,比如我用的 1.2.1_1 才有 gif2webp 的功能。

安装完之后在终端中能使用 cwebpgif2webp 这两个命令了,它们是具体负责生成 .webp 的工具。

然后我写了一个 shell 脚本来生成 .webp ,因为有可能生成的 .webp 图片比原图还要大,所以在脚本里判断了一下,如果比原图大就删掉。

list=$(find ./public ./src -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' -o -name '*.gif')

for originF in $list; do
  webpF=$originF.webp

  # -e 用来检查文件是否存在
  if [ -e $webpF ]; then
    continue
  fi

  if [[ $originF =~ gif$ ]]; then
    gif2webp $originF -o $webpF -quiet
  else
    cwebp $originF -o $webpF -quiet
  fi

  origin_size=$(ls -l $originF | awk '{print $5}')
  webp_size=$(ls -l $webpF | awk '{print $5}')

  osize=$(ls -lh $originF | awk '{print $5}')
  wsize=$(ls -lh $webpF | awk '{print $5}')

  if [ $webp_size -ge $origin_size ]; then
    rm $webpF
    echo "😯 dropped $webpF, $osize --> $wsize"
  else
    percent=$(awk 'BEGIN{printf "%.1f%%\n",(('$origin_size'-'$webp_size')/'$origin_size')*100}')
    echo "✅ generated $webpF, $osize --> $wsize, reduce $percent"
  fi
done

脚本执行效果如下,还挺好玩的吧 hhh 。

image

那 AVIF 又是什么?

恩,也是一种图片格式,比 WebP 出现得晚一些,有啥区别呢?

Using Modern Image Formats: AVIF And WebP — Smashing Magazine 这里有进行对比。

modern-image-formats-3

比如 4:4:4 和 HDR 这两个特性 WebP 就不支持,而 AVIF 支持。

如果你不知道这两个特性是啥的话,说明你们项目对图片没那么高要求,WebP 就够了,手动狗头,hhh

来个总结

使用 WebP 最省事的方式还是直接在 CDN 上做,如果 CDN 支持配置的话。

手动配置也还行,不是特别麻烦。

如果项目中有 banner 啊,或者别的大图片的话,使用了 WebP 后图片加载还是会快很多的。

至于要不要上 AVIF ,如果真的是做那种对图片质量要求很高的网站,可以试试,否则 WebP 其实就够了。

findxc commented 2 years ago

补充一下 WebP 有损时的图片质量

上面我们把 .png 生成 .webp 时用的命令是 cwebp xxx.png -o xxx.png.webp ,这样使用的是默认的压缩参数,并且是有损的。

对于有损 WebP 来说,如果图片有细线条或者高对比度的像素,这附近的细节丢失会比较明显,比如下面这种场景,文字周围会感觉有点糊。

image

就算把图片缩小点展示,文字周围也还是能看出差别。当然,如果文字颜色和背景色对比没这么强烈,差别就不会这么大。

image

有损 WebP 适用于没那么追求图片质量、更加在意图片体积的场景,因为一般会使用二倍图,所以一般肉眼看不出太多差别,比如B站、YouTube首页会有大量图片,就是用的有损 WebP 。

cwebp -lossless xxx.png -o xxx.png.webp 加上 -lossless 表示用无损的方式转换,这种转换出来 .webp 也会比 .png 体积小,并且图像质量是一致的。但是由于有 TinyPNG – Compress WebP, PNG and JPEG images intelligently 这个工具,用这个工具去压缩 .png 能得到比无损 WebP 更小的图片体积,并且图片质量也 ok 。

所以如果很追求图片质量,用 TinyPNG 去压缩会更合适。