If you have ever built a web game only to watch your pixelated hero stutter across the screen like a startled pigeon, you are already intimately acquainted with frame-rate drops. Building butter-smooth 60fps browser games sounds lovely in theory, but reality often involves sudden garbage collection pauses, heavy DOM reflows, and monitors refreshing at quirky rates like 144Hz or 90Hz.
With modern web games enjoying a massive resurgence across indie platforms and retro portals, getting your HTML5 canvas animation loop right is the difference between an addictive masterpiece and an unplayable motion-sickness simulator. Let us dive into the mechanics of building rock-solid game loops that keep your frame rates pristine.
Entity Definition: What is a Canvas Animation Loop?
Canvas Animation Loop Definition: A canvas animation loop is a continuous execution cycle in a web browser that repeatedly clears and redraws graphics onto an HTML5
<canvas>element. It synchronises game logic updates, physics calculations, and visual rendering to match the display's refresh rate—ideally targeting a steady 60 frames per second (fps).
The Cardinal Sin: Why setInterval Ruins Web Games
Back in the dark ages of web development, developers used setInterval or setTimeout to trigger frame updates. If you are still doing this, step away from the keyboard and take a deep breath.
Relying on timers for animation is a fool's errand. Browsers background tabs aggressively, timers drift due to the single-threaded nature of JavaScript event loops, and you end up burning CPU cycles rendering frames that the user cannot even see.
Instead, developers rely on requestAnimationFrame (rAF). This browser API tells the browser you wish to perform an animation and requests that the browser calls a specified function to update an animation right before the next repaint.
let lastTime = 0;
function gameLoop(timestamp) {
if (!lastTime) lastTime = timestamp;
const deltaTime = timestamp - lastTime;
// Always update game state with delta time
update(deltaTime);
render();
lastTime = timestamp;
requestAnimationFrame(gameLoop);
}
// Kick off the loop
requestAnimationFrame(gameLoop);
The Secret Sauce: Delta Time Physics
Here is a common trap: you tie your game speed directly to your frame rate. You write player.x += 5 every time the loop ticks, assuming everyone on earth plays your game on a rigid 60Hz display.
Then a player joins with a high-end gaming laptop running a 240Hz monitor, and suddenly your casual puzzle platformer looks like a hyperactive squirrel on double espresso. Conversely, someone on a budget mobile phone experiences a slow-motion tragedy.
The fix? Delta time ($\Delta t$).
Delta time measures the exact millisecond duration between the previous frame and the current frame. By multiplying your movement speeds by this time differential, your game becomes entirely frame-rate independent.
| Approach | Speed Consistency | High-Refresh Monitor Behaviour | Mobile Performance |
|---|---|---|---|
Fixed Step (x += 5) | Terrible | Runs 4x too fast on 240Hz | Chugs in slow motion |
| Delta Time Multiplier | Rock Solid | Butter-smooth at any Hz | Adjusts gracefully |
Implementing Delta Time
Here is how you scale your velocity vectors using delta time in your update step:
function update(deltaTime) {
// Convert deltaTime from milliseconds to seconds for saner math
const deltaSeconds = deltaTime / 1000;
// Player moves at a constant 300 pixels per second, regardless of frame drops
player.x += player.vx * deltaSeconds;
player.y += player.vy * deltaSeconds;
}
Community Insights: Fixing Jank and Micro-Stutters
If you lurk around the r/gamedev subreddits or check GitHub discussions on web performance, you will quickly notice a recurring villain: Garbage Collection (GC).
When your game loop instantiates new objects inside the render loop (like creating new vector objects or objects inside arrays), JavaScript engines eventually have to pause execution to sweep away the memory clutter. This causes those notorious micro-stutters that ruin an otherwise pristine 60fps experience.
Developer consensus across major web game post-mortems highlights three crucial optimisations:
1. Object Pooling: Pre-allocate your bullets, particles, and enemies in a static pool rather than spawning and destroying them on the fly.
2. Avoid DOM Thrashing: Never query layout properties (element.getBoundingClientRect()) inside your canvas update loop. Keep your UI elements separate from your canvas rendering pipeline.
3. Dirty Rectangles (When Applicable): For grid-based or turn-based browser games, only clear and redraw the specific canvas tiles that have actually changed, rather than wiping the entire viewport every single frame.
Key Takeaways
- Abandon Timers: Never use
setIntervalfor game loops; always userequestAnimationFrameto sync with the browser's repaint cycle. - Embrace Delta Time: Multiply your physics velocities by the time elapsed between frames to ensure consistent gameplay speeds across 60Hz, 120Hz, and variable mobile displays.
- Watch Memory Allocation: Keep object creation out of your core loop to prevent garbage collection hiccups from tanking your frame rate.