Lesson 09
Performance debugging: fix slow games before they freeze the browser.
Canvas can draw a lot, but not infinite objects forever. Performance debugging is mostly about measuring frame time, capping expensive systems and removing work the player cannot see.
1. Measure frame time, not vibes
If a game "feels slow," measure it. Keep a small history of frame times and compute an average. You do not need a full profiler to notice that a scene jumped from 16 ms to 45 ms per frame.
const perf = {
history: [],
averageFrameTime: 16.7
};
function trackFrame(now) {
if (!trackFrame.last) trackFrame.last = now;
const frameTime = now - trackFrame.last;
trackFrame.last = now;
perf.history.push(frameTime);
if (perf.history.length > 60) perf.history.shift();
const total = perf.history.reduce((sum, value) => sum + value, 0);
perf.averageFrameTime = total / perf.history.length;
}
Source pattern: Supagames physics and simulation pages use frame timing and `requestAnimationFrame` loops to watch expensive scenes.
At 60 FPS, you have about 16.7 ms for update and draw combined. If your average climbs above 30 ms, players will feel stutter. On mobile, you should be even more careful because CPU and battery limits are tighter.
2. Cap particles and projectiles
Particles are satisfying, but they are also a classic way to slow down a canvas game. Every particle must update, draw and eventually disappear. Always give particles a life value and cap the array.
const particles = [];
const MAX_PARTICLES = 180;
function spawnBurst(x, y, color) {
for (let i = 0; i < 12; i++) {
if (particles.length >= MAX_PARTICLES) particles.shift();
particles.push({
x, y,
vx: (Math.random() - 0.5) * 4,
vy: (Math.random() - 0.5) * 4,
life: 35,
color
});
}
}
function updateParticles() {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.life--;
if (p.life <= 0) particles.splice(i, 1);
}
}
Looping backward makes removal safe. The maximum count prevents a chain reaction from leaving thousands of invisible particles in memory.
3. Avoid unnecessary getImageData loops
Browser consoles often warn that repeated `getImageData` calls should use `willReadFrequently`. That warning matters. Reading pixels from a canvas can be expensive because it pulls data back from the graphics pipeline.
const analysisCanvas = document.createElement("canvas");
const analysisCtx = analysisCanvas.getContext("2d", { willReadFrequently: true });
function countColoredPixels() {
const image = analysisCtx.getImageData(0, 0, analysisCanvas.width, analysisCanvas.height);
let count = 0;
for (let i = 3; i < image.data.length; i += 4) {
if (image.data[i] > 0) count++;
}
return count;
}
If you need pixel analysis, use a separate analysis canvas, do it rarely and cache results. Do not read the entire canvas every frame unless the game is specifically built around image processing.
4. Debug the worst scene
Testing only the first screen is comforting and misleading. The worst scene is usually late-game: maximum enemies, projectiles, particles, UI, score updates and maybe a boss. Your QA pass should jump straight to the busiest moment.
function debugSpawnStressTest() {
enemies.length = 0;
projectiles.length = 0;
particles.length = 0;
for (let i = 0; i < 60; i++) spawnEnemyNearEdge();
for (let i = 0; i < 120; i++) spawnBurst(W / 2, H / 2, "#ffd93d");
console.log("Stress test:", {
enemies: enemies.length,
particles: particles.length
});
}
This kind of dev-only helper should not ship as a visible button, but it is useful while building. It catches performance traps before players find them.
5. Common Supagames performance fixes
- Stop update loops after game over if nothing needs animation.
- Remove off-screen bullets, pipes, enemies and particles immediately.
- Use one canvas clear per frame, not one clear per object.
- Cache static backgrounds when possible.
- Throttle expensive UI text updates if the value did not change.
- Check console errors first. A repeated exception can be worse than slow drawing.
6. Separate update cost from draw cost
If the game is slow, find out whether logic or rendering is the expensive part. Time them separately for a few frames. This turns "the game is laggy" into a concrete lead.
function frame(now) {
const t0 = performance.now();
update(now);
const t1 = performance.now();
draw();
const t2 = performance.now();
debug.updateMs = t1 - t0;
debug.drawMs = t2 - t1;
requestAnimationFrame(frame);
}
If update time is high, look at enemy AI, collision loops, pathfinding and array removal. If draw time is high, look at shadows, gradients, text rendering, huge canvases and repeated background work. If both are high, reduce object counts first.
7. Use object pools only when needed
Beginners sometimes reach for object pooling too early. It can help in projectile-heavy games, but it also adds complexity. Start with clean arrays and life timers. If profiling shows garbage collection spikes from thousands of short-lived objects, then pool the hottest type.
const bulletPool = [];
function getBullet() {
return bulletPool.pop() || { active: false, x: 0, y: 0, vx: 0, vy: 0 };
}
function releaseBullet(bullet) {
bullet.active = false;
bulletPool.push(bullet);
}
Do not use pooling as a ritual. Use it as a response to measured pressure. The cleanest code that runs smoothly is better than a clever system that nobody wants to debug.