HTML5 Game Memory Leaks: Find and Fix Them
HTML5 Game Memory Leaks: Find and Fix Them
Honesty note: the patterns below are the standard web-game leak classes; the DevTools workflow is how I’d debug my own game. “Leak” in a browser usually means retained memory growing until GC or tab death — the fix is almost always “stop holding references,” not “free memory” (you can’t; GC does).
TL;DR
- A browser-game leak is retained references, not unfreed memory. JS has GC — memory grows because something still points to dead objects. Find the reference, stop the growth (GC reality).
- The four real leak patterns in games: (1) event listeners never removed (scene destroy without
off/removeListener), (2) closures capturing scene/game state (timers or callbacks that outlive their scene), (3) textures/canvases not disposed (Phaser:texture.destroy()/ scene shutdown left assets resident), (4) global/stash collections (arrays you push into but never trim — e.g. a “all entities” list that keeps dead ones). - Find it with Heap Snapshot, not guessing: DevTools → Memory → Heap snapshot before/after a repeatable action (e.g. 20 scene restarts) — filter by
Detachedor look at retained size growth. The delta names the culprit. - The fix discipline: every
addEventListenergets a matching remove (oronce), every scene shutdown destroys its assets and clears timers, and hot lists are trimmed — the same checkpoint discipline that keeps code healthy keeps memory healthy.
The four patterns (with fixes)
1. Event listeners that outlive their object
// leak: added in scene A, never removed
window.addEventListener('resize', this.onResize);
// fix: remove on shutdown, or use 'once' for one-shots
window.removeEventListener('resize', this.onResize);
2. Closures capturing game state
// leak: timer callback holds 'scene' forever
setInterval(() => scene.updateHUD(), 1000);
// fix: clear on shutdown; prefer scene-owned timers (Phaser: this.time.delayedCall)
3. Undisposed textures / render targets
// Phaser: removing a sprite doesn't free its texture if something holds it
this.textures.remove(key); // or destroy owned textures on scene shutdown
For Godot/Canvas: offscreen canvases created per frame and never released are the same class (asset lifecycle).
4. Ever-growing collections
// leak: entities array grows forever
this.entities.push(sprite); // dead sprites still in the list
// fix: filter/trim, or use pools that recycle ([pool tip](/blog/html5-game-fps-boost-tips))
The finding workflow (20 minutes)
- Open DevTools → Memory → Heap snapshot (with game in a stable state).
- Do a repeatable action (restart the level 10×, open/close the shop 20×).
- Take snapshot 2, filter
Detachednodes, or compare retained size. - The class name that keeps appearing (Scene, Texture, Timer) is the leak’s source; go find where it’s held (snapshot reality).
The cheap check: watch Task Manager / Performance monitor memory during a long session — steady growth across 10 minutes of play is the practical signal, even before snapshots.
Why “leak” is usually fixable in a session
Unlike native engines, a browser leak isn’t a memory-allocation bug — it’s a reference graph bug, and DevTools shows you the graph. The four patterns above cover most game cases; fixing them is mechanical once the snapshot names the holder (the engineering mindset).
Pitfalls
- Hunting GC “leaks” — GC is not leaking; retained references are. Look at what holds dead objects.
- Removing listeners but not timers — timers/callbacks are the sneaky holder; clear all on shutdown.
- Destroying sprites but not textures — the texture outlives the scene if shared or cached (atlas lifecycle).
- Pushing into lists without trimming — collections grow silently; pool or filter.
- Guessing instead of snapshotting — a 20-minute snapshot session beats a day of suspicion (measure first).
Bottom line
HTML5 game memory leaks are retained references, and the fix is finding the holder: remove listeners, clear timers/closures, dispose textures, trim collections — then verify with heap snapshots before/after a repeatable action. It’s the same discipline as the rest of engineering: measure, find the holder, cut the reference (the full optimization method).