Step 4: Quality & Performance
19. スマホが熱くなる?
Canvas画像処理の
メモリ管理と最適化
画像の圧縮ファイルの容量と、展開した画素のメモリは別です。作業Canvasの寸法を抑え、不要なURLや画像の参照を残さないことが基本になります。このサイトの音楽プレイヤー風・ガラケー風加工にある1500 px上限を、実装上の工夫として確認します。
1. Canvas は想像以上の「メモリ食い」
ブラウザ上で高度な画像加工を可能にする Canvas API ですが、実は非常にメモリを消費する仕組みになっています。通常、JPEGなどの画像ファイルは圧縮された状態で保存されていますが、Canvas に描画(展開)される際は、一般的な8ビットRGBAの作業バッファでは、1ピクセルあたり4バイトが目安になります。画像のデコード結果やGPU側のバッファなどもあるため、この計算だけでブラウザ全体の使用量は決まりません。
例えば、最近のスマートフォンで撮影した 1200万画素(4000x3000px)の写真をそのまま Canvas に読み込むと、RGBAの1バッファだけでも 4000 × 3000 × 4 = 48,000,000バイト(約48MB、約45.8MiB)が目安になります。複数の大きなバッファを同時に保持すると、利用可能なメモリを使い切るOOM(Out Of Memory)や、タブの終了につながる場合があります。
2. 解決策:URL.revokeObjectURL による明示的な解放
Object URLは、文書を閉じるかURLを破棄するまで、URLから参照できるデータを保持します。不要になった参照を残すと、繰り返し操作で保持量が増える原因になります。
これを解決するのが URL.revokeObjectURL() 命令です。
最適化の Before / After
【Before】メモリリークが発生する書き方
// 写真を切り替えるたびに新しいURLが発行され、古いデータがメモリに残る
const onFileChange = (file) => {
const url = URL.createObjectURL(file);
previewImg.src = url;
// ⚠️ 前のURLが破棄されないため、メモリ消費が増え続ける
}; 【After】リソースを適切に解放する書き方
let currentUrl = null;
const onFileChange = (file) => {
// 1. 以前のURLによる参照を解除する(即時の全メモリ解放ではない)
if (currentUrl) {
URL.revokeObjectURL(currentUrl);
}
// 2. 新しいURLを発行して保持する
currentUrl = URL.createObjectURL(file);
previewImg.src = currentUrl;
}; 3. 注意点:初心者がハマりやすい「見えないメモリ」
メモリ管理において、特に注意が必要なポイントをいくつか挙げます。
- Canvas要素の再利用: 毎回
document.createElement('canvas')をして巨大なキャンバスを作るのではなく、用途とサイズが同じCanvasを再利用すると、確保の繰り返しを減らせます。ただし clearRect は画素を消す操作で、Canvasのバッファ容量を減らす操作ではありません。 - 解像度の制限: ブラウザやOSには「Canvasの最大サイズ」という物理的な制限があります。極端に巨大な画像を読み込もうとすると、描画が失敗するだけでなく、タブが即座にクラッシュすることがあります。
- Blobの扱い:
toBlob()で生成されたデータも、不要になったら保持している参照を外すことが重要です。1つの変数をnullにしても、配列・クロージャ・画像要素などが参照していれば回収されません。
4. 端末の資源と保持するデータを考える
サーバーサイドのチューニングでは「スワップが発生しないようにメモリを空ける」のが鉄則ですが、フロントエンドは「ユーザーが所有する端末という、限られたリソースを借りて動いている」という側面があります。
利用者の端末で動くため、入力の上限、保持する履歴、書き出し時の一時データを設計します。処理速度や発熱は端末・負荷に依存するので、実機で確認した条件を残して判断します。
縮小の効果と、残るメモリを見積もる
4000×3000pxを長辺1500pxへ縮小すると1500×1125pxです。4バイトRGBAの1バッファは約6.75MBとなり、元の約48MBより小さくなります。これは作業バッファ1枚分の計算で、元画像、プレビュー、書き出しデータを同時に保持すれば、その分も必要です。
toDataURL() は画像全体を文字列として生成します。大きな画像では toBlob() とBlob URLの利用も検討できますが、保存操作やブラウザ互換性を含めて確認する必要があります。結果画面から再編集する場合にも、元画像を残すコストを考えます。
確認する操作
- 同じ大きさの画像で、選択・加工・保存を数回繰り返す。
- 画像を差し替えた後も、古いImage・Canvas・Blob URLへの参照が残っていないか調べる。
- 実機で操作の応答、タブの再読み込み、書き出し失敗を確認する。メモリ値だけで正常と決めない。
二つの加工ツールが、読み込み後の長辺を1500 pxへ抑える
音楽プレイヤー風加工は、元画像のデコード後に長辺1500 pxの上限を計算し、作業Canvasと加工用画像へ渡します。ガラケー風加工にも同じ長辺上限があります。
const MAX_SIZE = 1500;
let w = img.width, h = img.height;
if (w > MAX_SIZE || h > MAX_SIZE) {
const scale = MAX_SIZE / Math.max(w, h);
w = Math.floor(w * scale);
h = Math.floor(h * scale);
}これは音楽プレイヤー風加工の寸法計算を抜き出したものです。縦横比を保ち、小さい画像は拡大しません。元画像は先にデコードするので、この上限だけで読み込み時のピーク負荷まで抑えられるとは言えません。音楽プレイヤー風の制作解説・ガラケー風の制作解説で後続の描画を確認できます。
一時Object URLは読み込み完了・失敗時に解放していますが、縮小後の画像やData URLは編集のために保持します。URLを破棄したことと、全画像メモリが直ちに空いたことは違います。下のRGBA計算は作業バッファの見積もりで、実端末の使用量・発熱を測定した結果ではありません。
5. まとめ:実務での役立ち
猫の手ツールの音楽プレイヤー風・ガラケー風加工では、入力画像を読み込んだ後に長辺1500pxを上限として縮小する処理があります。作業用の画素数を抑える工夫ですが、元画像のデコード時の負荷や端末差までなくなるわけではありません。
入力上限を設け、同じ操作を繰り返しても不要な参照が増えないか確認します。計算上のバッファサイズだけでなく、操作の応答と保存の成否も合わせて見ます。