前回の続き
前回、Canvas APIだけで画像をリサイズする実装を書いた。
動きはするんだけど、大きめの画像を読み込んだときに
UIがちょっと固まる感覚があって気になっていた。
原因はわかっていて、drawImage の処理がメインスレッドで走るから。
今回はそれを Web Worker に逃がして、UIをブロックしないようにする。
Web Worker の基本的な考え方
Web Worker は、メインスレッドとは別のスレッドで JS を動かす仕組みです。
ただし Worker 内では DOM にアクセスできないので、
Canvas の描画処理を丸ごと Worker に渡すには OffscreenCanvas を使います。
メインスレッド Worker
ファイル読み込み → OffscreenCanvasで描画
UI の更新 ← 処理結果(Blob)を返す
コード
worker.js
self.onmessage = async (e) => {
const { bitmap, maxWidth, maxHeight } = e.data;
const ratio = Math.min(maxWidth / bitmap.width, maxHeight / bitmap.height, 1);
const width = Math.round(bitmap.width * ratio);
const height = Math.round(bitmap.height * ratio);
const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap, 0, 0, width, height);
const blob = await canvas.convertToBlob({ type: 'image/png' });
self.postMessage({ blob, width, height });
};
main.js
const worker = new Worker('worker.js');
input.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
const bitmap = await createImageBitmap(file);
worker.postMessage(
{ bitmap, maxWidth: 800, maxHeight: 800 },
[bitmap] // transferable として渡す(コピーではなく転送)
);
});
worker.onmessage = (e) => {
const { blob, width, height } = e.data;
const url = URL.createObjectURL(blob);
download.href = url;
download.textContent = `ダウンロード(${width}×${height})`;
};
[bitmap] の部分について
postMessage の第2引数に [bitmap] を渡しているのは、
データをコピーではなく「転送」するためです。
大きな画像データをコピーして渡すとその分メモリが2倍使われますが、
転送するとメインスレッド側の参照が消えて Worker 側に所有権が移ります。
画像が大きくなるほどここの差が効いてくるので、忘れずに書いておく方がいいです。
OffscreenCanvas の対応状況
2024年時点で Chrome・Edge・Firefox は対応済み。
Safari は16.4から対応しているので、古い iOS 端末だと動かないケースがあります。
対応ブラウザを絞れる環境なら気にしなくていいですが、
不特定多数に使わせる場合はフォールバックを用意しておく方が無難です。
自分の場合は Worker が使えるかどうかを判定して、
ダメならメインスレッドにフォールバックするようにしました。
if (window.Worker && 'OffscreenCanvas' in window) {
// Worker で処理
} else {
// 前回の Canvas API 実装にフォールバック
}
次は複数ファイルをまとめて処理する実装を書きます。
自分でゼロから実装するのが大変な場合は、
同じ仕組みで動くブラウザツール BulkPicTools も作っているので、
よかったら使ってみてください → https://bulkpictools.com