公開
Duel Simulatorを長時間使うと重くなる問題をどう直したか
Duel Simulatorの長時間利用でメモリが増える問題を、10,000回の盤面操作で再現しました。履歴内の操作参照950万件を差分とチェックポイントへ変え、上限11,000件に抑えた設計と検証方法を解説します。
Duel Simulatorを長時間使い続けるとタブが重くなり、最後はChromeごと落ちるという報告がありました。 コード調査では放置中に増え続けるタイマー処理は見つからず、盤面操作を積み重ねる手順では増加を再現できました。 経過時間ではなく、操作履歴の量が手掛かりでした。
修正前の実装へ同じカードの表裏切替を10,000回入力すると、履歴から参照される操作は950万件まで増えました。 履歴の件数自体は1,000件に制限されていたため、一見すると上限は機能していました。 問題は、1件の履歴が抱える中身に上限がなかったことです。
同じ盤面操作を繰り返して増え方を測る
長時間待つ代わりに、Duel Simulatorの状態管理をNode.js上で読み込み、同じカードの表裏切替を連続実行しました。
各測定は操作を0回から作り直し、明示的にGCを実行した後で、履歴内の操作参照数、heapUsed、RSSを記録しています。
操作参照数は、保持している履歴配列の全要素からたどれる操作の延べ件数です。 同じ操作オブジェクトを複数の配列が参照する場合も別々に数えるため、メモリ量そのものではなく、履歴構造の増え方を調べる指標です。
| 盤面操作 | 操作参照数 | heapUsed | RSS |
|---|---|---|---|
| 0回 | 0件 | 9.3 MiB | 86.3 MiB |
| 1,000回 | 500,500件 | 14.9 MiB | 102.6 MiB |
| 5,000回 | 4,500,500件 | 47.3 MiB | 307.6 MiB |
| 10,000回 | 9,500,500件 | 87.9 MiB | 567.3 MiB |

数値は修正前コードを使ったローカルの再現テスト1回の結果です。 絶対値は実行環境で変わるため、ブラウザのタブ使用量ではなく、操作数に伴う増加傾向として読んでください。 測定値のCSV には、同じ条件で測った修正後の値も収録しています。
1,000件の上限が950万参照を残した理由
修正前の履歴エントリは、その時点までの操作配列を毎回コピーしていました。 10,000回目のエントリは10,000件、9,999回目のエントリは9,999件の操作参照を持ちます。
履歴を最新1,000件に切り詰めても、残る配列の長さは9,001件から10,000件です。 その合計は次の式で9,500,500件になります。
(9,001 + 10,000) × 1,000 ÷ 2 = 9,500,500件
履歴エントリ数は一定でも、各エントリが持つ累積配列は操作数とともに長くなります。 このため、履歴上限は「配列の本数」を抑えても、「配列内の参照数」を抑えられていませんでした。
累積コピーを差分とチェックポイントへ変える
修正後は、既存のoperationCountに加えてoperationStartを各履歴エントリへ持たせました。
通常のエントリが保存するのは、直前の履歴から増えた操作だけです。
古い履歴を上限から押し出すときは、最初に残るエントリをチェックポイントにします。 このエントリだけが最新の操作ログ配列を共有参照し、その後の999件は原則として各1件の差分を持ちます。
10,000操作後に保持し得る操作参照数は、先頭の10,000件と直近差分999件を合わせた10,999件です。 回帰テストでは、余裕を含む条件として「操作数10,000件と履歴上限1,000件の合計以下」を検証しています。 修正前の9,500,500件と比べると、参照数の上限は約864分の1です。
操作列を復元するときは、目的のエントリから必要な差分を逆向きに集め、元の順序へ戻します。 通常のUndoとRedoは各エントリが持つ盤面スナップショットを使い、状態を持たないリプレイ履歴だけを直前のチェックポイントから再計算します。 保存時の軽さだけでなく、Undo、Redo、途中分岐で元の操作列を再構成できることが設計条件です。
既存のリプレイチェックポイントを有限の履歴と組み合わせる
リプレイでは、操作列に加えて盤面状態そのものも大きくなります。 すべての操作後に完全な盤面を保存すればシークは速い一方、長いリプレイほど同じ状態を大量に抱えます。
リプレイには以前から、完全な盤面状態を25操作ごとのチェックポイントへ間引く仕組みがありました。 今回の修正では、その間隔を変えずに履歴を最新1,000件へ制限し、古い履歴を押し出した後の先頭へ復元可能な盤面状態を置きます。 任意位置へ移動するときは、直前のチェックポイントから必要な操作だけを再生します。 これにより、保持量を制限しながら、先頭への移動や途中再生を維持できます。

メモリ対策後も、盤面操作、Undo、Redo、リプレイ保存と再生の使い方は変わりません。
履歴以外の解放漏れも同時に塞ぐ
履歴が最大の増加要因でしたが、長時間利用では小さな解放漏れも積み重なります。 同じ修正で、スクリーンショット用Object URL、カードDOMの登録表、画像認識用OCR Workerのライフサイクルも整理しました。
スクリーンショットのObject URLは、ダウンロード開始後だけでなく、途中で例外が起きた場合にも解放します。 小さい画像をData URLとして返す経路では、不要なObject URLを作らないようにしました。
カードDOMの登録表は、最後の要素を解除した時点で空のキーも削除します。
OCR Workerは処理要求を直列化し、成功と失敗のどちらでもfinallyから終了させます。
リソースを作った処理が解放まで担当する形に揃えることで、画面側の呼び忘れを減らしました。
メモリ削減と操作の正しさを同じテストで守る
履歴形式を変えると、メモリは減ってもUndoやリプレイが壊れる危険があります。 そこで10,000操作後の参照数だけでなく、復元した操作列が元の10,000件と一致することも同じテストで確認しています。
リプレイについては、履歴件数とチェックポイント数の上限、連続再生、シーク、停止、エラー後の復帰を検証しました。 通常操作についても、Undo、Redo、途中からの分岐、リセット、録画中の操作列が一致することを確認しています。
上限値だけをテストすると、データを捨てて軽くする誤実装でも成功してしまいます。 保持量の条件と復元結果の条件を組にすることで、「軽いが戻れない」履歴を防げます。
修正後の同じ手順では10,999参照に収まった
修正を含む現行コードへ同じNode.jsスクリプトを実行すると、10,000操作後の参照数は10,999件でした。
heapUsedは18.6 MiB、RSSは164.3 MiBです。
| 盤面操作 | 操作参照数 | heapUsed | RSS |
|---|---|---|---|
| 0回 | 0件 | 13.5 MiB | 113.6 MiB |
| 1,000回 | 1,999件 | 14.2 MiB | 105.8 MiB |
| 5,000回 | 5,999件 | 16.2 MiB | 126.5 MiB |
| 10,000回 | 10,999件 | 18.6 MiB | 164.3 MiB |
修正前の測定日は2026年8月23日、修正後の再測定日は2026年9月4日で、どちらも各操作数を別プロセスで実行しています。
Node.jsの起動時点だけでもRSSには約27 MiBの差があり、GCやランタイムの確保状況によっても数十MiB単位でぶれます。
このため、異なる日の絶対値からメモリ削減率を求めることはできません。
一方、コードから数えられる操作参照数は同じ定義で9,500,500件から10,999件へ減り、操作数に対するheapUsedの増加も小さくなりました。
修正は2026年8月25日にmainブランチへ入り、2026年9月4日に本番配信ファイルへ新しい履歴構造が含まれることを確認しました。
長時間利用の不具合から得た設計上の教訓
コレクションの件数上限は、メモリ上限と同じではありません。 各要素が累積データを抱える場合は、コンテナ数に加えて、内部の参照数やバイト数を測る必要があります。
長時間待つ不具合は、時間を短縮できる操作量へ置き換えると再現しやすくなります。 今回の表裏切替テストは実際の状態更新経路を使うため、単純な配列だけのベンチマークより原因へ近い指標になりました。
差分保存は、復元方法とチェックポイント間隔まで決めて初めて使える設計です。 Duel Simulatorでは保持量、復元精度、シーク時の再計算量を別々に検証し、長時間の展開練習でも操作履歴を使い続けられる形へ直しました。
盤面操作の応用ガイドではUndoとスクリーンショットを、リプレイの共有ガイドでは保存と再生の手順を確認できます。 ツール全体の役割と技術構成は4つの遊戯王ツールを作った理由にまとめています。 Duel Simulatorの機能一覧からは、基本ガイドを含む関連ページをたどれます。
参考情報
記事内容に誤りを見つけた場合は お問い合わせ から報告してください。