猫の手ツール

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の利用も検討できますが、保存操作やブラウザ互換性を含めて確認する必要があります。結果画面から再編集する場合にも、元画像を残すコストを考えます。

確認する操作

  1. 同じ大きさの画像で、選択・加工・保存を数回繰り返す。
  2. 画像を差し替えた後も、古いImage・Canvas・Blob URLへの参照が残っていないか調べる。
  3. 実機で操作の応答、タブの再読み込み、書き出し失敗を確認する。メモリ値だけで正常と決めない。

二つの加工ツールが、読み込み後の長辺を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を上限として縮小する処理があります。作業用の画素数を抑える工夫ですが、元画像のデコード時の負荷や端末差までなくなるわけではありません。

入力上限を設け、同じ操作を繰り返しても不要な参照が増えないか確認します。計算上のバッファサイズだけでなく、操作の応答と保存の成否も合わせて見ます。

参考資料

制作時に確かめたこと

ツールを作る途中で実際に確認した症状と、変更を選んだ経緯です。日付は当時の記録日です。現在の実装や、このページの学習サンプルの検証とは分けて読んでください。

スマホで放置した画面へ戻ると、エラーになった

サイズ上限を検討した後も、復帰時の確認が残った

3月30日、スマホでツールを開いたままにするとブラウザエラーになることを報告しました。そこで、入力が上限を超えた時だけ縦横比を保って縮小し、整数にした寸法をCanvasへ設定してから描く改良を依頼しています。当時の依頼文は上限値を指定するためのひな形で、全ツール共通の実測限界を示す数値ではありません。

4月26日にも、ページを残して後からブラウザへ戻るとエラーになる場合があり、更新では戻れず、ブラウザの戻る操作で前のページが表示されると記録しています。メモリを疑っていますが、計測によって原因を確定した記録ではありません。

現在の1500 px上限は、本文で取り上げた二つのツールのコードにある設定です。これを過去のエラーが全件直った証拠や、スマホで必ず安全な寸法と読み替えず、選択・加工・保存に加えて放置後の復帰も確認します。