在网页里点一下「导出 AVIF」,图是一张 1200 万像素的照片,要等将近四秒文件才出来。这是我在自己搭的测试页上量到的,机器是一台 Apple M4 的 Mac,系统是 macOS,浏览器是 Chromium 149,三次分别是 3802、3754、3747 毫秒。我日常的活绕不开商品图上传,想知道把转 AVIF 这一步挪到浏览器里做,一张图要等多久,换成多线程又能省下多少,就先拿一张图试了试。
开始之前把几个词说清楚。AVIF 是一种比较新的图片格式。把一堆像素压成某种格式的文件,干这个活的程序叫编码器。我试的四个浏览器里有三个不会自己往外写 AVIF,所以页面得自带一个编码器。我用的是 @jsquash/avif 2.1.1,它把 libavif v1.0.1 这个库编成了 WASM。WASM 可以理解成浏览器能直接运行的一种二进制程序,别的语言写的库靠它搬进网页。这个包里的编码器有两份,一份单线程,一份多线程。线程就是干活的人手,单线程好比一摞卷子只有一个老师在改,多线程是把卷子分给几个老师同时改。我这台机器在 Chromium 里报的是 10 个核,我原先想当然地觉得换成多线程那份能快上好几倍。量出来没有那么多。
用的图是维基共享资源上 Jakub Fryš 拍的一张湖景,CC BY-SA 4.0 授权。图不是我拍的。我在测试页里把它缩成五档大小,参数全用这个包的默认值,也就是质量 50、speed 6、4:2:0,每档编三次。Chromium 149 里,200 万像素单线程是 696、696、673 毫秒,多线程是 327、324、323 毫秒。800 万像素是 2.6 秒对 1 秒出头。1200 万像素就是开头那个 3.8 秒左右,多线程三次是 1441、1410、1468 毫秒,约 1.4 秒。1600 万像素是 4.9 秒对 1.85 秒上下。Chrome 155 的读数和 Chromium 几乎贴在一起。WebKit 26.5 的多线程还略快一点,1200 万像素是 1279、1260、1251 毫秒。按各档的中间值算,多线程在 Chromium 和 Chrome 里快 1.9 到 2.7 倍,在 WebKit 里快 2.0 到 3.2 倍,说成快两三倍差不多。

图里每一档上面那条是单线程,下面那条是多线程,标的是三次里的中间值。看 1200 万像素那一档就够了,3,754 毫秒对 1,441 毫秒。
这些毫秒数量的只是 encode() 这一个调用,解码原图、缩放、取像素、把结果交回页面都没算进去。编码我都放在 Worker 里跑,Worker 是浏览器给网页开的后台线程。测试页里挑文件和计时的就是下面几行:
js
const glue = useThreads ? 'avif_enc_mt.js' : 'avif_enc.js';
const enc = await loadEnc(glue);
const t0 = performance.now();
const bytes = enc.encode(pixels, width, height, { ...defaults, speed: 6 });
const cost = Math.round(performance.now() - t0); // 只算这一个调用
我用的 Firefox 151 测试版本里,两份编码器都慢出一大截。正式发布版我没复核,原因也没查,这一篇就不把它算进倍数里。
多线程那份不是换个文件名就能用,它要求页面处在「跨源隔离」状态。说白了就是服务器返回页面的时候多带两个响应头,响应头是服务器随内容一起发回来的几行说明。浏览器看到之后会把这个页面和别的来源隔开,页面里 crossOriginIsolated 这个值就变成 true。我的测试页是本机起的一个小服务,返回每个文件时都带着 Cross-Origin-Opener-Policy: same-origin 和 Cross-Origin-Embedder-Policy: require-corp 这两行,四个浏览器里这个值都是 true。不带这两个头的时候,多线程那份是报错,是悄悄退回单线程,还是别的什么表现,我没有测,这里不乱说。另外这两个头是对整个页面生效的。我的测试页上全是自己的文件,页面里要是嵌了别的域名的图片、脚本或者 iframe,会不会受影响?这个我也没试。改响应头在我这儿算影响面比较大的改动,真要上之前我会把这一项单独验一遍。
快 2 到 3 倍还有一个前提,就是 speed 停在默认的 6。speed 是编码器的一个档位,数字越大编得越快。我把同一张图固定在 200 万像素,把 speed 调到 8 和 10 各跑了三次。speed 8 时单线程是 230、163、161 毫秒,多线程是 239、174、173 毫秒。speed 10 时单线程是 156、103、100 毫秒,多线程是 204、149、146 毫秒。拿第二次的读数比,174 对 163,149 对 103,多线程不比单线程快,speed 10 那一档还慢了一些。为什么会这样我没去查。画质也要补一句,speed 8、10 编出来的画质我没有量,这两组数只能说明线程的事,不能拿来当「该把 speed 调高」的依据。
我还拿一个现成的在线工具看了看它带的是哪一份编码器。我用的是图映 ImgIng(imging.cn/),一个在线处理图片的... Chromium 149 里把那张湖景的五档 PNG 各转了一次 AVIF。从网络请求看,它在我选中 AVIF 的那一刻就开始拉编码器,拉的是 avif_enc_mt.wasm,也就是多线程那份,gzip 压缩后传输 1,135,337 字节,约 1.1 MB。第一次转换时按钮上会先出现「初始化核心...」,后面几次没有再出现。从点下转换到按钮恢复,1200 万像素那档等了 1579 毫秒,1600 万像素那档等了 1630 毫秒。这两个数不能和我上面那些对着比,口径不一样。它是我从页面外面掐的整段等待,读图和界面更新都在里面,画质默认停在 52,和我的 50 也不一样,每档也只跑了一次。它内部怎么写的我没有看。

这是第一次转换中途截的右侧面板。AVIF 按钮角上标着「按需 WASM」,画质滑杆停在 52,下面的大按钮正显示「初始化核心...」,最底下一行写着模块下载完成、正在编译本地编码核心。
回到我自己的事上。我后来定下来的顺序是,先在页面的控制台里打一下 crossOriginIsolated,是 false 就先不考虑多线程那份。是 true 的话,再用自己业务里常见尺寸的图,把两份编码器在打算用的 speed 上各跑三次,看倍数还在不在。两份文件体积差不多,单线程 3,485,872 字节,多线程 3,534,665 字节,我自己用 gzip 压了一下都在 1.1 MB 上下。选哪份主要看那两个头加不加得上。这些数我只在一台 M4 上量过,换一台机器、换一个浏览器版本都会变。没有那两个头时的表现也还空着。