毎フレーム作って壊すのをやめる(オブジェクトプールの効き目を実測)
弾と敵が同時に数百個出るゲームを作りました。表示のたびに GameObject を作って壊す書き方と、作ったものを使い回す書き方を、同じ場面で測って比べます。1フレーム 19.06ms が 0.15ms になり、確保するメモリは 0 バイトになりました。
公開 2026年9月19日
この記事の要点4 points
- 同じ場面(画面に361個)で測ると、19.06ms → 0.15ms。 60fps の持ち時間 16.7ms を、作って壊す書き方だけで使い切る。
- 確保するメモリは1,904,640バイト → 0バイト(60フレームぶん)。 掃除が走る=カクつきの元。
- 使い回すのは絵(GameObject)だけではない。 中身の配列も作り直さない。
- 🔴 ただしふだんの場面では差が出ない。 数十個なら、どちらでも 60fps だった。
目次6章
検証した環境最終確認 2026年9月19日
- Windows 11 / RTX 2080 Ti
- Unity 6.6(6000.6.0f1)・WebGL ビルド
- Claude Code(Opus 5 / 思考レベル 高)
書いてあるのは、この環境で実際に試した結果です。ツールの仕様や料金は変わるので、重要な判断の前には公式の情報も確認してください。
弾と敵が同時に数百個出るゲームを作りました。ここで初めて「毎フレーム作って壊す」が問題になります。
同じゲーム・同じ場面で、2つの書き方を測りました。
19.06ms
作って壊す
1フレーム(361個)
0.15ms
使い回す
同じ場面
1.9MB
作って壊すと確保
60フレームぶん
0バイト
使い回すと確保
同じ60フレーム
何を比べたか
比べたのは描画だけです。ゲームの中身(弾や敵の位置を進める処理)は同じで、画面に出す部分だけを2通り書きました。
毎フレーム作って壊す
- フレームの最初に、前のフレームのものを全部 Destroy
- いま必要な数だけ GameObject を作る
- 書くのは簡単。数が少ないうちは何も起きない
作ったものを使い回す(プール)
- 作ったものは持っておく。足りないときだけ作る
- 余ったぶんは消さずに隠す
- 位置と大きさを入れ直すだけ
使い回すほうの中身は、これだけです。
public SpriteRenderer Next(float x, float y, float diameter)
{
SpriteRenderer sr = _used < _items.Count ? _items[_used] : Create(); // 足りないときだけ作る
_used++;
sr.transform.localPosition = new Vector3(x, y, 0f);
sr.transform.localScale = new Vector3(diameter, diameter, 1f);
if (!sr.gameObject.activeSelf) sr.gameObject.SetActive(true);
return sr;
}
public void End()
{
for (int i = _used; i < _items.Count; i++) // 使わなかったぶんは「隠す」だけ
{
if (_items[i].gameObject.activeSelf) _items[i].gameObject.SetActive(false);
}
}
測った結果
画面に敵60・自機の弾300・自機1=361個が出ている状態で、描画を60回くり返して測りました。
画面にあるもの:敵60 自機の弾300 合計361個 プールあり: 60回 9.0ms(1回 0.15ms)確保 0バイト プールなし: 60回 1143.8ms(1回 19.06ms)確保 1904640バイト プールあり: 60回 8.8ms(1回 0.15ms)確保 0バイト
60fps を保つには、1フレームを 16.7ms に収める必要があります。作って壊す書き方は、描画だけでそれを超えました。しかもフレームごとに約31KB を確保するので、そのうち掃除(GC)が走ります。
同じ絵を出すのに、127倍の時間がかかっていた。
中身(ロジック)側も同じ
使い回すのは絵だけではありません。弾や敵の入れものも、毎フレーム作り直さないようにしています。
このゲームでは、弾・敵を最初に確保した配列に入れ、生きているものだけを前に詰めています。消すときは、最後のものを穴に移して数を1減らすだけです。
// 消し方:並べ替えない・new しない
void RemoveShot(int i) => _shots[i] = _shots[--_shotCount];
void RemoveEnemy(int i) => _enemies[i] = _enemies[--_enemyCount];
これで「消えた印を見に行く」無駄も、詰め直しの並べ替えも要らなくなります。効いているかどうかは、テストで見張っています。
[Test]
public void 進めるあいだ新しくメモリを取らない()
{
...
long before = GC.GetTotalMemory(true);
for (int i = 0; i < 6000; i++) battle.Tick(Frame, input);
long after = GC.GetTotalMemory(true);
Assert.That(after - before, Is.LessThanOrEqualTo(0L), $"6000フレームで {after - before} バイト増えた");
}
テストにすると、あとから戻らない
速さの工夫は、あとから誰か(AI も)が普通の書き方に戻してしまうのが怖いところです。「確保が増えたら落ちるテスト」にしておくと、戻った瞬間に分かります。
ふだんの場面では差が出なかった
正直に書くと、遊んでいる最中の画面では差が出ませんでした。実際に出ているのは数十個で、ブラウザで測るとどちらも 60fps・最長フレーム 16.9ms。
{"url":"https://aigame.game-union-libra.com/play/mureuchi/","fps":60.1,"worstMs":16.9,"frames":601}
{"url":"https://aigame.game-union-libra.com/play/mureuchi/?nopool=1","fps":60.1,"worstMs":16.9,"frames":601}ブラウザの描画は 60 回/秒で頭打ちになるので、余裕がどれだけあるかは fps には出ません。差を見るには、数を上限まで増やした場面(361個)で、1フレームにかかる時間そのものを測る必要がありました。
⚠️ 「fps 60 だから大丈夫」ではない
fps は頭打ちの数字です。余裕を知りたいなら、1フレームにかかった時間(や最長フレーム)を見てください。余裕が無い状態は、スマホなど遅い機械でそのまま処理落ちになります。
この数が出ている画面

私は最初、絵の出し方を「毎フレーム作り直す」で書きかけました。数十個なら本当に何も起きないからです。使い回す形に最初からしたのは、あとから直すと描画まわりを書き直すことになると分かっていたためで、速さのためではありません。効き目の数字は、あとで測って初めて分かりました。
まとめ
- 画面に361個ある場面で、19.06ms → 0.15ms・確保は1.9MB → 0バイト
- 使い回すのは絵だけでなく、中身の配列も。消すときは最後のものを穴に移す
- 効き目はテストで固定する(確保が増えたら落ちる)
- ただし数十個の場面では差が出ない。fps ではなく、1フレームの時間で見る
FAQ
よくある質問
オブジェクトプールは、どれくらい効きますか?
このゲーム(画面に361個)では、描画の1フレームが 19.06ms から 0.15ms になりました。確保するメモリも 60フレームで 1,904,640 バイトから 0 バイトになります。ただし数十個しか出ない場面では、どちらの書き方でも 60fps でした。
何を使い回せばいいですか?
毎フレーム作り直しているものすべてです。GameObject や SpriteRenderer はもちろん、中身(ロジック)側で new している配列やリストも同じです。数が増減するものは、配列を固定の大きさで持ち、使う数だけ前に詰めます。
消すときはどうしますか?
消さずに隠します(SetActive(false))。中身の配列では「最後のものを穴に移して数を1減らす」形にすると、並べ替えも「消えた印」も要りません。
最初から凝ったほうがいいですか?
数が少ないうちは差が出ません。ただし「毎フレーム作って壊す」は後から直すと描画まわりを書き直すことになるので、数が増えると分かっているなら最初から使い回す形にしておくのが楽です。
この記事は役に立ちましたか?
NEXT
あわせて読む
Unity WebGLビルドの容量を27%削減した方法(10.1MB→7.4MB)
まず設定2つで 10,143,315 → 7,945,839 バイト。次に未使用パッケージ39件を外して 7,374,207 バイト。さらに使っていないシェーダー9本を落として 7,311,364 バイト。合計 −27.9% です。効かなかった施策(スプラッシュ停止)も数字ごと残します。
Unity WebGLの読み込み時間とfpsを測る方法(軽量化の前後で比較)
ダウンロード 0.76秒、起動 0.6秒、プレイ中 60.1fps。10.1MB 版と 7.37MB 版を同じ条件で比べたら起動が19%速くなりました。計測用ページの作り方と、圧縮を外したあとのサイズという見落としがちな数字、そして fps の計測でつまずいた話です。
Webゲームの進行を保存する(何を保存しないか・壊れても落ちない形)
ブラウザで遊ぶパズルに「どこまでクリアしたか」と自己ベストを保存しました。盤面は保存しません。保存先がブラウザの領域であること、壊れた保存データで落ちない書き方、記事を書いていて気づいた「版が無い」という穴と、その直し方を残します。