個人的に 2012年頃(?)から渇望していた OffscreenCanvas と一連のAPIがついに Chrome に実装されました。
このエントリでは、UIの応答性を改善する OffscreenCanvas の仕組みと、 OffscreenCanvas を有効活用するための周辺APIについて、概要とサンプルコードを紹介していきます。
ネタ元はこちらです https://www.youtube.com/watch?time_continue=159&v=wkDd-x0EkFU
既存の <canvas>
は DOM と強く結びついている事から UI Thread(= ブラウザにおける Main Thread) の影響をうけますし、反対に影響を与えてしまいます。
影響: UI Thread で重い処理を走らせてしまうと、アニメーションのフレームスキップが発生し、なめらかにスクロールしなくなり、UIの応答性が悪くなるなどの弊害が発生してしまいます。
以下の新しいブラウザAPIを使用することで、これまで発生していた応答性に関する課題を改善することが可能になります。
const offcan = new OffscreenCanvas(width, height);
image.decode().then(() => {...});
const image = createImageBitmap(blob);
const offcan = canvas.transferControlToOffscreen(); worker.postMessage({ offcan }, [offcan]);
offcan.transferToImageBitmap()
image.decode()
は image.onload
に代わるAPIです。
従来から多用されてきた image.src = url; image.onload = () => {}
は、
イメージのデコード処理を UI Thread で行うため、
大きな画像をデコードした場合に UI がギクシャクしてしまいます。
const image = new Image();
image.src = url;
image.onload = () => {
// use
};
この課題を解決するため Chrome 64 で image.decode()
が追加されています。
image.decode()
はイメージのデコードを別スレッドで行い Promise を返します。
これで画像の大きさに応じてUIの応答性が悪化する可能性は無視できるほど小さくなるはずです。
const image = new Image();
image.src = url;
image.decode().then(() => { // decode by other thread
// use
});
画像の非同期読み込み用に、 <image lazyload="auto/on/off">
という属性も最近追加されています。
https://www.bleepingcomputer.com/news/google/built-in-lazy-loading-lands-in-google-chrome-canary/
WebGL や 2D Canvas で使用する画像を効率的に作成するため Chrome 50 で createImageBitmap()
が追加されています。
(async() {
const image = createImageBitmap( await ( await fetch(url) ).blob() );
// use image
})();
(async() {
const resp = await fetch(url);
if (resp.ok) {
const blob = await resp.blob();
const image = createImageBitmap(blob);
// use image
}
})();
ImageBitmap は canvas に ctx.drawImage(bitmap)
として貼り付ける事ができます。
新しいAPI new OffscreenCanvas(width, height)
を使うと DOM から切り離された Canvas を作成することができます。
DOM から切り離すという点が重要でする。DOM から切り離された事により以下のメリットが得られます。
- すべてが Canvas で構成されている場合は DOM Tree と Canvas を合成するための時間が不要になります。ゲームでは特に有効でしょう
- 専用の Worker でレンダリングが可能になりました。時間がかかる処理を別スレッドで実行できる事になり、UI Thread が混雑していても影響を受けません
- UI Thread と Worker のやり取りはポインタのコピーの形で行われます。ゼロコピーとして実装されており、とても効率的です
以下の例では、DOM Tree 上に存在する <canvas class="surface">
を OffscreenCanvas でレンダリングできるように
Workerにポインタをシェアしています。
// main.js
const canvas = document.querySelector(`.surface`); // <canvas class="surface"></canvas>
const offcan = canvas.transferControlToOffscreen(); // pointer
const worker = new Worker(`worker.js`);
worker.postMessage({ offcan: offcan }, [ offcan ]);
// worker.js
const ctx;
self.onmessage = (msg) => {
ctx = msg.data.offcan.getContext(`2d`);
update();
};
const update = () => {
ctx.commit().then(update);
};
Worker上でbitmapを作成し、UI Thread や WebGL 側で bitmapを使用することができます。こちらもゼロコピーです
// worker.js
const offcan = new OffscreenCanvas(width, height);
const offctx = offcan.getContext(`2d`);
// rendering to offctx
const image = offcan.transferToImageBitmap();
self.postMessage({ image: image }, [ image ]); // 移譲(imageの所有権は受け取った側に移ります)
canvas.getContext("bitmaprenderer")
が追加されており、bitmap イメージの送受信が可能になっています。
const snapshot = backgroundCanvas.transferToImageBitmap();
const ctx = viewCanvas.getContext(`bitmaprenderer`);
ctx.transferFromImageBitmap(snapshot);
OffscreenCanvas は Chrome 70 で実験的に追加された Shape Detection API と組み合わせて使用するとさらに強力になるでしょう。
//TODO: Sample code