🕳️

[JavaScript] drawImageがどんどん遅くなるときはsaveとrestoreの数を数えてみる

2021/11/21に公開

概要

JavaScriptのcanvasを用いてゲームを作るときは次のようにしている人が多いと思います.

  • 見えないキャンバス(visibility:hiddenをつけたものや,動的に生成したものなど)に色々描画
  • その結果を見えるキャンバスにdrawImageを用いてコピー
  • 上の処理を繰り返し

このdrawImageが繰り返す毎に重くなっているときは,saveしている数がrestoreしている数よりも多くなっているかも知れません.

発見

PureScriptでゲームを作成するライブラリを作っていたとき,その動作テスト中にどんどんfpsが落ちる事案が発生.DevToolsで見たところdrawImageの処理がじわじわと遅くなっていっていました.6時間ほどの探索の末,描画処理をしているモジュールの中でsaverestoreよりも多かったことが判明しました.

何故見つけるのに時間がかかったか

  • 処理を多くの関数に分けていたため全て正確に追えていなかった.
  • 描画された見た目は問題なかったため,saverestoreが原因だと気づかなかった.
  • saverestoreの数はデバッグで数えるのが難しい.

再現

<!DOCTYPE html>
<html>
    <head>
        <script async="" type="text/javascript" src="index.js"></script>
    </head>
    <body>
        <canvas id = "canvas"></canvas>
    </body>
</html>
const canvas = document.getElementById('canvas');
canvas.width = 300.0;
canvas.height = 300.0;
const ctx = canvas.getContext('2d');

const off_canvas = document.createElement('canvas');
off_canvas.width = 300.0;
off_canvas.height = 300.0;
const off_ctx = off_canvas.getContext('2d');

const mainLoop = () => {
  off_ctx.save();
  off_ctx.save(); //2回save

  off_ctx.clearRect(0.0, 0.0, 300.0, 300.0);
  off_ctx.fillRect (100.0, 100.0, 100.0, 100.0);

  off_ctx.restore(); //1回restore

  ctx.clearRect(0.0, 0.0, 300.0, 300.0);
  ctx.drawImage(off_canvas, 0.0, 0.0);

  setTimeout(mainLoop, 1.0);
}

mainLoop();

DevToolsで見ればこのように処理が遅くなっていることが分かります.

  • 開始時

  • 暫く経った後

再発防止

与えられた処理をsaveとrestoreで囲んでくれるような関数を作って,利用することにします.
例(PureScript)

saveAndRestore ctx f = do
  save ctx
  f
  restore ctx

この関数以外の場所でsaverestoreを書かないようにすればよいです.

restore > saveの場合

特に遅くなるなどの変化はなりませんでした.しかしそもそもsaverestoreの数が異なるのは健全なプログラムでは無さそうなので,きちんと揃えた方が良さそうです.

Discussion