Web动画性能优化 - 图片解码与Image.decode API

背景介绍

今天在阅读谷歌 web-vitals 源码的时候发现其中有使用到一个 Image.decode API,查阅 MDN 文档得到如下解释:

HTMLImageElement.decode()

HTMLImageElement 接口的 decode() 方法返回一个当图片解码后可安全用于附加到 DOM 上时 resolves 的 Promise 对象。 这可用于在将图片附加到一个 DOM 中的元素(或作为一个新元素加入 DOM 中)之前启动加载,所以在将图像添加到 DOM 时可以立即渲染图像。这反过来,防止了将图像加入 DOM 后图像的加载造成下一帧渲染的延迟。

一个 decode() 的潜在用例:当在加载一个非常大的图片时(例如,一个在线相册),你可以在加载初期提供一个低分辨率的缩略图,之后通过实例化一个 HTMLImageElement 将该图像替换为一个全分辨率图像,设置其 source 为全分辨率图像 URL,使用 decode() 获取一旦全分辨率图像准备好被使用时 resolved 的 promise 对象。这时你可以使用当前可用的全分辨率图像替换之前的低分辨率图像。

来源:https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLImageElement/decode

简单来说该 API 可以用来提前加载解析 Image,避免图片在屏幕上闪动的问题。提到图片预加载,我们第一时间想到的也是之前经常使用到的方法是利用 onload 回调函数进行判断,那这两者之间又有什么区别呢?

示例分析

通过一段代码分析一下 decode 和 onload 的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const img = new Image();
img.onload = function (img) {
console.log("onload: " + performance.now());
console.log(img);
};
img.onerror = function (err) {
console.log("onerror: " + performance.now());
console.log(err);
};
img.src = "https://xxx/logo.png";
img
.decode()
.then(() => {
console.log("resolve: " + performance.now());
document.body.appendChild(img);
})
.catch((encodingError) => {
console.log("catch: " + performance.now());
console.log(encodingError);
// Do something with the error.
});

加载成功时:

加载成功

加载失败时:

加载失败

加载失败

两者对比

  1. 从 API 设计层面上讲,decode 原生使用 Promise Api 进行封装的,编写更方便,但兼容性上会稍微差一点。
  2. onload/onerror 事件触发时会返回一个 event 对象,而 decode 方法 resolve 不返回信息,error 只返回”DOMException: The source image cannot be decoded.”信息。
  3. 当图片正常加载时,onload 触发的时机会比 decode 回调更早(这一点后面再具体分析)。
  4. 当图片加载失败时,两者触发时机相近。

原理分析

在两者对比的时候发现一个两者最主要的区别就是 onload 触发的时机会比 decode 回调更早,那么两者相差的时间到底在哪儿?

我们仔细阅读 MDN 的说明后发现其中有一个关键词“当图片解码后”,也就是说这个 API 不仅仅做了“加载图片”这一件事,还做了“图片解码”的任务,等图片解码后才执行了回调。

关于图片解码浏览器渲染流程可以参考这两篇文章。

再具体场景中要体现出两者的区别,主要是在非合成器动画中合成器在动画的绘制过程中需要等待图片解码任务的完成,故会造成动画卡顿的现象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<!DOCTYPE html>
<body>
<img
id="arrow"
src="arrow.png"
style="will-change: transform; transform: rotate(0deg)"
/>
<script>
// prepareImage函数分别使用decode和onload进行封装,对rAF动画效果的影响
function prepareImage() {
var img = new Image();
img.src = "nebula.jpg";
img.onload = function () {
document.body.appendChild(img);
};
}
function prepareImage() {
var img = new Image();
img.src = "nebula.jpg";
img.decode().then(function () {
document.body.appendChild(img);
});
}
// -----------------------------------------------
// Change this to change revolution delay.
var revolutionTimeMs = 2000;

var angle = 0.0;
var dAnglePerMs = 360.0 / revolutionTimeMs;
var arrowElement = document.getElementById("arrow");
var done = false;

// Rotate the arrow.
function advanceArrow(deltaMs) {
angle += dAnglePerMs * deltaMs;
// On first full revolution, add the image.
if (angle >= 360) {
done = true;
prepareImage();
}
while (angle >= 360) {
angle -= 360;
}
arrowElement.style.transform = "rotate(" + angle + "deg)";
}

var lastTimestamp = 0;
function go(timestamp) {
if (!lastTimestamp) lastTimestamp = timestamp;
var deltaMs = timestamp - lastTimestamp;
advanceArrow(deltaMs);

lastTimestamp = timestamp;
window.requestAnimationFrame(go);
}

window.requestAnimationFrame(go);
</script>
</body>

具体效果差异可以看这个视频:https://cdn.jsdelivr.net/gh/suzhihao/wobushi.top@master/assets/20210201/demo.ogv

总结

浏览器渲染流水线解析与网页动画性能优化这篇文章也可以看出,当代浏览器对于合成器动画的优化已经是非常好的了,这也是为什么我们推荐优先考虑使用 CSS 动画,而在必须使用 js 动画的情况下,优先使用 rAF 进行动画的同时,还需要考虑非常多的因素,最终达到的效果可能也没办法达到 CSS 动画那么流畅。该文希望能够在解决列表加载图片的滚动场景中卡顿的问题提供帮助。

附录:兼容性

兼容性


Web动画性能优化 - 图片解码与Image.decode API
https://www.wobushi.top/2021/Web动画性能优化-图片解码与Image-decodeAPI/
作者
Pride Su
发布于
2021年2月1日
许可协议