Image Decode API: 支持异步解码
2018-10-29
本文是公众号推送的第一篇关于Image Decode API的文章,本文主要从简单实际的用例的来介绍该API。
# 什么是Image Decode API
这是一个whatwg标准,允许图片进行预解码,以此解决因图片解码所导致的光栅化卡顿问题。其大致使用方法如下:
var img = new Image();
img.src = '...';
// The decoded image is guaranteed to be kept in memory until the frame
// after the decode promise resolves.
img.decode().then(function() {
// This is guaranteed to paint the image without flicker or decoding jank.
document.body.appendChild(img);
});
# 为什么需要Image Decode API
网页中使用的图片是进过编码压缩,所以在Web页面解析渲染过程中,图片需要有一个解码的过程。从现在的浏览器内核来看,需要图片解码一般发生在如下两个场景:
- 页面排版:某些场景下,排版引擎需要获取图片真实高宽,并以此计算出图片在页面中所占据的区域。该场景下,通常仅需要解析图片头部信息,获取高宽信息即可,无需对整个图片进行完整解码,故而非常高效;
- 图片渲染:在页面需要绘制展现图片时,则需要对图片进行完整解码,解码一般发生在光栅化线程,会同步阻塞页面光栅化。对于大图片来讲,解码需要一定的时间,这意味着在大图片解码过程中,很可能会出现卡顿现象,如下例子:
<!doctype HTML>
<body>
<img id="arrow" src="arrow.png" style="will-change: transform; transform: rotate(0deg);" />
<script>
function prepareImage() {
var img = new Image();
img.src = "nebula.jpg";
img.onload = 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>
从上图可以看到,在图片解码过程中,动画出现了明显的卡顿!那问题该如何处理呢?或许有人会说:“那使用createImageBitmap进行异步解码吧,然后把图片解码后的bitmap通过Canvas绘制出来!” 没错,这确实能够解决卡顿问题,但它也存在一些问题,特别是内存问题。而Image Decode API: img.decode的出现,不仅支持了异步解码解决了卡顿问题,也规避掉了createImageBitmap所存在的局限性。
# 如何使用Image Decode API
<!doctype HTML>
<body>
<img id="arrow" src="arrow.png" style="will-change: transform; transform: rotate(0deg);" />
<script>
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>
运行结果如下:
这个案例中,我们只修改了一行代码
img.onload = function() { document.body.appendChild(img); }; // 旧代码
----------------------------------------
img.decode().then(function() { document.body.appendChild(img); }); // 新代码
你可以理解为: img.onload,意味着加载完成后append图片元素,当该图片需要光栅化时(显示),图片在主线程中进行解码 img.decode(),意味着在单独的线程中提前解码,完成解码后append图片元素
那么,是否将所有图片都改成提前异步解码就更好?这其实是个内存及性能的权衡问题:如果所有图片都预先解码,性能肯定较优,但内存开销太大(可能有些图片最终并不需要展现),而如果所有图片都在需要绘制的时候才解码,内存当然较优,可带来的后果则是绘制时的性能损害(需要解码)。从api制定的思路来看,Web引擎是希望把异步解码的控制权交回给页面开发者,开发者可以结合自己实际的业务,针对特定的场景进行优化,寻求一个适合自身业务情况的最优解决方案。