Why do modern games run so smoothly in a browser, capturing users’ time as seamlessly as any desktop application? The secret lies in the JavaScript game loop.

This concept isn’t just about displaying images. It’s about managing updates and rendering graphics at a controlled frame rate while ensuring smooth interactions.

As someone who loves designing interactive experiences, I’m aware that mastering the game loop is key to developing engaging and fluid web games.

Whether you’re using HTML5, tweaking for CPU load, or optimizing with WebGL, understanding the game loop lays the groundwork for success.

By the end of this article, you’ll understand how the game loop coordinates game logic with real-time events and user input efficiently.

We’ll delve into essential aspects such as using RequestAnimationFrame API for optimal performance and managing the balance between graphics rendering and computational load.

Plus, get insights into integrating event listeners and enhancing JavaScript performance efficiently.

What is a JavaScript game loop?

A JavaScript game loop is a programming pattern used to create smooth and continuous gameplay in web games.

It handles the repeated execution of game logic, updating game states, processing user input, and rendering graphics in real time. Essentially, it’s what keeps a game running efficiently.

Understanding Frame Rates and Performance

Frames Per Second (FPS) and Its Impact

Definition of FPS and Why It Matters

FPS (Frames Per Second) forms the backbone of JavaScript gaming performance. It represents the number of times your game updates and renders each second. Higher FPS creates smoother animations and better gameplay feel.

The relationship between frame rate and user experience is direct – games running at higher FPS respond better to user input and appear more fluid.

Popular frameworks like Phaser.js and Three.js incorporate sophisticated frame management systems to help developers maintain consistent FPS across different devices and browsers.

The Relationship Between FPS and Perceived Smoothness

Frame synchronization affects how players experience your game. When your game maintains 60+ FPS, animations flow naturally without perceptible stuttering.

Browser rendering engines process each frame individually, so maintaining high FPS ensures:

  • Responsive controls
  • Smooth character movement
  • Consistent physics simulation
  • Realistic animation timing

The human eye perceives motion at roughly 24 FPS, but interactive applications benefit from higher rates since they need to account for real-time input.

Common FPS Targets for Games

Different game types have varying FPS requirements:

  • 30 FPS: Minimum acceptable for casual browser games with limited animation
  • 60 FPS: Standard target for most HTML5 games using Canvas API
  • 120+ FPS: For competitive or physics-heavy games where precise timing matters

Tools like Chrome DevTools provide frame rate metrics to help developers monitor performance during development.

The Ideal Frame Rate for Game Loops

The Limitations of Hardware and Display Refresh Rates

Your game loop architecture must account for hardware constraints. Not all devices can handle high frame rates due to:

  1. CPU processing power
  2. Browser JavaScript engine efficiency (V8 Engine or others)
  3. Display refresh rate limits
  4. Mobile battery considerations

WebGL integration can offload rendering to the GPU, but computational graphics still face bottlenecks based on the user’s hardware.

Games built with PixiJS or Babylon.js automatically adjust their loop execution speed based on device capabilities.

Balancing High FPS with System Resource Usage

Performance tuning requires finding the sweet spot between visual quality and resource consumption.

Web Workers can handle background tasks without affecting your main game loop performance.

Consider these strategies:

  • Implement frame skipping when necessary
  • Use time-based movement instead of frame-based
  • Optimize sprite animation and asset loading
  • Monitor JavaScript performance metrics during development

Common Approaches to Implementing Game Loops

Using Basic Loops (Inefficient Methods)

The naïve while (true) loop approach and why it fails

The simplest approach – a continuous while(true) loop – causes major issues:

function startGame() {
  while(true) {
    updateGame();
    renderGame();
  }
}

This blocks the JavaScript event model completely. The browser becomes unresponsive because:

  1. The event loop gets stuck
  2. DOM manipulation can’t occur
  3. User inputs are never processed
  4. The browser may crash entirely

No professional JavaScript game design pattern uses this approach.

Using setInterval() for a timed loop

Many beginners try setInterval() for their game loop:

setInterval(function() {
  updateGameLogic();
  renderFrame();
}, 16); // ~60fps

While better than the while loop, this method has serious flaws:

  • Doesn’t sync with browser rendering cycles
  • Continues running when the tab isn’t active
  • Produces inconsistent timing
  • Leads to lag compensation issues

Game engines like Construct 3 abandoned this approach years ago.

Introduction to requestAnimationFrame()

How requestAnimationFrame() synchronizes with the browser’s rendering

YouTube player

The Web Animation API introduced requestAnimationFrame() as the solution to frame management:

function gameLoop() {
  updateGameState();
  renderGame();
  requestAnimationFrame(gameLoop);
}

requestAnimationFrame(gameLoop);

This method aligns your game loop with the browser’s natural rendering cycle.

Mozilla Developer Network documentation highlights these benefits:

  • Browser optimizes animations behind the scenes
  • Pauses when users switch tabs (saving resources)
  • Adapts to screen refresh rate automatically
  • Provides smooth frame transitions

Advantages over setInterval() and traditional loops

Using requestAnimationFrame() offers significant performance advantages:

  1. Natural synchronization with display refresh rates
  2. Reduced CPU usage when the game isn’t visible
  3. Better battery life on mobile devices
  4. Consistent timing between frame updates
  5. Improved compatibility across modern browsers

This is why all serious JavaScript game libraries like React Game Kit, GreenSock Animation Platform, and Kontra.js use this approach.

Example of a basic game loop using requestAnimationFrame()

Here’s a practical implementation with delta time calculation:

let lastTimestamp = 0;

function gameLoop(timestamp) {
  // Calculate time since last frame
  const deltaTime = timestamp - lastTimestamp;
  lastTimestamp = timestamp;

  // Update game state based on time passed
  updateGameLogic(deltaTime);

  // Render the current frame
  renderFrame();

  // Schedule the next frame
  requestAnimationFrame(gameLoop);
}

// Start the loop
requestAnimationFrame(gameLoop);

This pattern works well with both HTML5 Canvas and WebGL rendering approaches.

Key Components of an Effective Game Loop

Updating the Game State

Handling movement, physics, and AI logic

The update function manages all dynamic aspects of your game:

function updateGameState(deltaTime) {
  updatePlayerPosition(deltaTime);
  updateEnemyAI(deltaTime);
  simulatePhysics(deltaTime);
  checkCollisions();
  processGameEvents();
}

For complex physics, consider libraries integrated with your chosen game engine:

  • Phaser uses its own physics system
  • Three.js can integrate with physics libraries
  • Custom solutions need careful time-step management

Different physics systems (arcade, realistic) require different update approaches.

Ensuring consistency in game logic updates

Consistency matters more than raw speed. Your game mechanics should behave predictably regardless of frame rate fluctuations.

Time-based movement is critical:

// Bad: Frame-dependent movement
player.x += 5; // Moves 5px every frame

// Good: Time-based movement
player.x += 300 * deltaTime; // Moves 300px per second

This approach ensures game state management remains consistent across devices with different performance levels.

Rendering the Game Frame

Redrawing game elements on the canvas or DOM

Rendering transforms your game state into visible output:

function renderFrame() {
  // Clear the canvas
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Draw background
  renderBackground();

  // Draw game objects
  renderEntities();

  // Draw UI elements
  renderInterface();
}

Modern browser rendering engines optimize for canvas operations, but DOM manipulation can be expensive.

Consider these approaches based on game type:

  • 2D sprite-based games: Canvas API
  • 3D games: WebGL
  • UI-heavy games: Combination of Canvas and limited DOM

Avoiding unnecessary rendering for performance optimization

Smart rendering saves processing power:

  1. Only redraw elements that changed
  2. Use layers for static elements
  3. Implement object culling (don’t render off-screen)
  4. Batch similar drawing operations

Indie game development often benefits from these optimization techniques when targeting mobile browsers.

Managing Timing and Delta Time

Understanding delta time for smooth animations

YouTube player

Delta time represents the milliseconds elapsed since the last frame:

function update(deltaTime) {
  // deltaTime is in milliseconds (e.g., 16.67ms at 60fps)

  // Convert to seconds for easier math
  const deltaSeconds = deltaTime / 1000;

  // Move at 100 pixels per second regardless of frame rate
  player.x += 100 * deltaSeconds;
}

This approach ensures animation smoothness even when frame rates drop.

The ECMAScript timing functions, combined with requestAnimationFrame, provide precise measurement tools.

Ensuring time-based updates rather than frame-based updates

Frame-based updates create inconsistent gameplay experiences. A jump might reach different heights depending on the device’s performance.

Time-based systems fix this:

// Frame-based (problematic)
function updateJump() {
  if (jumping) {
    player.y -= jumpSpeed;
    jumpSpeed -= gravity;
  }
}

// Time-based (consistent)
function updateJump(deltaTime) {
  if (jumping) {
    player.y -= jumpSpeed * deltaTime;
    jumpSpeed -= gravity * deltaTime;
  }
}

Game Programming Patterns suggests standardizing all movement calculations with delta time to ensure consistent gameplay across devices.

Advanced Game Loop Techniques

Fixed Timestep vs. Variable Timestep

The difference between fixed and variable timestep approaches

Game loops come in two timing styles that handle the flow of your game differently:

Variable timestep updates game states based on actual time passed since the last frame. This works well for most visual elements:

function update(deltaTime) {
  // Time-based movement
  player.x += speed * deltaTime; // Units per second
}

Fixed timestep runs updates at steady intervals regardless of your actual frame rate:

const FIXED_STEP = 1/60; // 60 updates per second
let accumulator = 0;

function gameLoop(currentTime) {
  let deltaTime = (currentTime - lastTime) / 1000;
  lastTime = currentTime;
  accumulator += deltaTime;

  // Run as many fixed updates as needed
  while (accumulator >= FIXED_STEP) {
    update(FIXED_STEP);
    accumulator -= FIXED_STEP;
  }

  render();
  requestAnimationFrame(gameLoop);
}

Popular libraries like Three.js and Babylon.js support both approaches, though they might emphasize one over the other.

Benefits of using a fixed timestep for physics calculations

Physics needs consistency above all. Fixed timesteps offer:

  • Predictable simulation results
  • Reliable collision detection
  • Consistent forces and acceleration
  • Reproducible behavior across devices

Games built with Phaser.js often use fixed timesteps for physics while using variable timesteps for animations and visual effects.

Handling Timing Issues and Synchronization

Using timestamps for accurate updates

Precise timing makes all the difference. Modern browsers offer high-resolution timing through the Performance API:

// Much more precise than Date.now()
const timestamp = performance.now();

// Calculate accurate delta time
const deltaTime = (timestamp - lastFrameTime) / 1000; // in seconds
lastFrameTime = timestamp;

The Mozilla Developer Network recommends using performance.now() for game development because it’s not affected by system clock changes and provides microsecond precision.

Addressing inconsistencies in frame rates

Frame rates jump around based on browser workload, hardware stress, and background processes. Handle this by:

  1. Capping maximum delta time
    // Prevent huge time jumps (like when tab was inactive)
    const MAX_DELTA = 0.1; // 100ms maximum
    const deltaTime = Math.min((now - lastTime) / 1000, MAX_DELTA);
    
  2. Using a countdown for critical timing
    // For timed events that must be precise
    timer -= deltaTime;
    if (timer <= 0) {
    fireEvent();
    timer = EVENT_INTERVAL;
    }
    
  3. Tracking average performance

Interpolation for Smooth Rendering

Why interpolation helps with visual smoothness

When physics runs at 60 fixed updates per second but your screen refreshes at 144Hz, you need interpolation to fill the gaps. This prevents choppy movement and creates smooth visual flow.

Without interpolation:

  • Objects appear to jump from position to position
  • Movement looks robotic and unnatural
  • High refresh rate displays show stuttering

Independent game developers use interpolation to achieve AAA-quality smoothness even in browser games.

Implementing interpolation within the game loop

Here’s how to add interpolation to a fixed-timestep loop:

function gameLoop(timestamp) {
  // Normal time stepping code...

  // Calculate how far between physics steps we are
  const alpha = accumulator / FIXED_STEP;

  // Render with interpolation
  render(alpha);

  requestAnimationFrame(gameLoop);
}

function render(alpha) {
  // For each object, interpolate between its previous and current state
  for (const entity of entities) {
    const renderX = entity.previousX + (entity.currentX - entity.previousX) * alpha;
    const renderY = entity.previousY + (entity.currentY - entity.previousY) * alpha;

    drawSprite(entity.sprite, renderX, renderY);
  }
}

This approach is used by libraries like GreenSock Animation Platform for smooth transitions.

FPS Control and Optimization

Monitoring and adjusting FPS dynamically

Keep track of your game’s performance:

// Simple FPS counter
let frameCount = 0;
let lastSecond = 0;
let fps = 0;

function updateFPS(timestamp) {
  frameCount++;

  if (timestamp - lastSecond > 1000) {
    fps = frameCount;
    frameCount = 0;
    lastSecond = timestamp;

    // Log or display current FPS
    console.log(`Current FPS: ${fps}`);

    // Adjust quality based on performance
    if (fps < 30) {
      lowerGraphicsQuality();
    } else if (fps > 55 && graphicsQuality < maxQuality) {
      increaseGraphicsQuality();
    }
  }
}

Chrome DevTools and the Web Performance API offer more advanced monitoring options.

Strategies for reducing performance overhead

When performance drops, adapt:

  • Skip rendering for off-screen objects
  • Reduce physics precision for distant entities
  • Scale back particle effects
  • Use simpler collision shapes
  • Implement object pooling for frequently created/destroyed objects

Progressive Web Apps that include games often use these techniques to adjust to different devices.

Handling User Input in the Game Loop

Capturing Keyboard and Mouse Events

Using event listeners for keydown and keyup

See the Pen
Game Input Demo
by Bogdan Sandu (@bogdansandu)
on CodePen.

Listen for keyboard input with event handlers:

// Track active keys
const keyState = {};

window.addEventListener('keydown', (e) => {
  keyState[e.code] = true;
});

window.addEventListener('keyup', (e) => {
  keyState[e.code] = false;
});

Touch events need similar handling for mobile browser games:

let touchActive = false;
let touchX = 0;
let touchY = 0;

canvas.addEventListener('touchstart', (e) => {
  touchActive = true;
  updateTouchPosition(e.touches[0]);
});

canvas.addEventListener('touchmove', (e) => {
  updateTouchPosition(e.touches[0]);
});

canvas.addEventListener('touchend', () => {
  touchActive = false;
});

function updateTouchPosition(touch) {
  const rect = canvas.getBoundingClientRect();
  touchX = touch.clientX - rect.left;
  touchY = touch.clientY - rect.top;
}

JavaScript game design should account for both input types.

Mapping input to game actions

Create an abstraction layer between inputs and game actions:

const ActionType = {
  PRESSED: 0,  // Just pressed this frame
  HELD: 1,     // Continuously held down
  RELEASED: 2  // Just released this frame
};

const actionMap = {
  'move_left': ['ArrowLeft', 'KeyA'],
  'move_right': ['ArrowRight', 'KeyD'],
  'jump': ['Space', 'KeyW', 'ArrowUp'],
  'shoot': ['ControlLeft', 'MouseLeft']
};

// Check if an action is active
function isActionActive(actionName, type = ActionType.HELD) {
  const keys = actionMap[actionName];

  if (!keys) return false;

  return keys.some(key => {
    if (type === ActionType.HELD) {
      return keyState[key];
    } else if (type === ActionType.PRESSED) {
      return keyState[key] && !prevKeyState[key];
    } else if (type === ActionType.RELEASED) {
      return !keyState[key] && prevKeyState[key];
    }
  });
}

Libraries like Kontra.js provide input handling systems that work across platforms.

Integrating Input with the Update Cycle

Storing input states for consistent behavior

Keep track of both current and previous input states:

let currentInput = {};
let previousInput = {};

function updateInputState() {
  // Save previous state
  previousInput = {...currentInput};

  // Update current state
  currentInput = {
    left: keyState['ArrowLeft'] || keyState['KeyA'],
    right: keyState['ArrowRight'] || keyState['KeyD'],
    jump: keyState['Space'],
    // More inputs...
  };
}

function wasJustPressed(action) {
  return currentInput[action] && !previousInput[action];
}

This approach catches inputs even if they happened between frames.

Processing inputs in sync with game logic

Process inputs at a specific point in your update cycle:

function update(deltaTime) {
  // Update input state first
  updateInputState();

  // Handle one-time actions
  if (wasJustPressed('jump') && player.canJump) {
    player.velocity.y = -JUMP_FORCE;
    player.canJump = false;
    playSound('jump');
  }

  // Handle continuous actions
  if (currentInput.left) {
    player.velocity.x = -MOVE_SPEED;
    player.direction = -1;
  } else if (currentInput.right) {
    player.velocity.x = MOVE_SPEED;
    player.direction = 1;
  } else {
    // Decelerate when no direction pressed
    player.velocity.x *= FRICTION;
  }

  // Update physics after input processing
  updatePhysics(deltaTime);
}

This creates responsive controls regardless of frame rate fluctuations.

Performance Optimization Strategies

Reducing CPU and GPU Load

Minimizing redundant calculations in the update cycle

Cut unnecessary work:

// Only update active entities
for (const entity of activeEntities) {
  // Skip if too far from player
  if (!isNearPlayer(entity)) {
    continue;
  }

  // Skip if nothing changed
  if (!entity.needsUpdate) {
    continue;
  }

  updateEntity(entity, deltaTime);
}

Use a spatial grid to quickly find nearby objects:

// Divide world into cells
const CELL_SIZE = 64;
const spatialGrid = {};

function addToGrid(entity) {
  const cellX = Math.floor(entity.x / CELL_SIZE);
  const cellY = Math.floor(entity.y / CELL_SIZE);
  const cellKey = `${cellX},${cellY}`;

  if (!spatialGrid[cellKey]) {
    spatialGrid[cellKey] = [];
  }

  spatialGrid[cellKey].push(entity);
}

function getNearbyEntities(x, y, radius) {
  const nearby = [];
  const cellRadius = Math.ceil(radius / CELL_SIZE);

  const centerCellX = Math.floor(x / CELL_SIZE);
  const centerCellY = Math.floor(y / CELL_SIZE);

  for (let cx = centerCellX - cellRadius; cx <= centerCellX + cellRadius; cx++) {
    for (let cy = centerCellY - cellRadius; cy <= centerCellY + cellRadius; cy++) {
      const key = `${cx},${cy}`;
      if (spatialGrid[key]) {
        nearby.push(...spatialGrid[key]);
      }
    }
  }

  return nearby;
}

This makes collision detection much faster in large worlds.

Using efficient rendering techniques

Optimize drawing operations:

  • Use sprite batching “`javascript // Group sprites by texture const batches = {};

for (const sprite of sprites) { if (!batches[sprite.textureId]) { batches[sprite.textureId] = []; } batches[sprite.textureId].push(sprite); }

// Draw each batch with a single call for (const textureId in batches) { ctx.bindTexture(textureId); drawSpriteBatch(batches[textureId]); }


* Create layers for different update frequencies
```javascript
// Background rarely changes
const backgroundLayer = document.createElement('canvas');
drawBackground(backgroundLayer.getContext('2d'));

// Main game updates every frame
const gameLayer = document.createElement('canvas');

// UI updates when scores change
const uiLayer = document.createElement('canvas');

function render() {
  // Clear only the game layer
  clearCanvas(gameLayer);

  // Draw game objects
  renderEntities(gameLayer.getContext('2d'));

  // Compose final frame
  ctx.drawImage(backgroundLayer, 0, 0);
  ctx.drawImage(gameLayer, 0, 0);
  ctx.drawImage(uiLayer, 0, 0);
}

WebGL rendering through Three.js provides even more performance benefits.

Using Web Workers for Background Tasks

Offloading non-critical computations

Move heavy work to background threads:

// Create a worker
const physicsWorker = new Worker('physics-worker.js');

// Send game state to worker
function updatePhysics() {
  physicsWorker.postMessage({
    entities: getSerializableEntities(),
    deltaTime: deltaTime
  });
}

// Receive updated state from worker
physicsWorker.onmessage = function(e) {
  updateEntitiesFromWorker(e.data.entities);
};

The worker script handles heavy calculations:

// physics-worker.js
self.onmessage = function(e) {
  const {entities, deltaTime} = e.data;

  // Run physics simulation
  simulatePhysics(entities, deltaTime);

  // Send updated entities back
  self.postMessage({entities: entities});
};

function simulatePhysics(entities, deltaTime) {
  // Complex physics calculations here
  // ...
}

This keeps your main thread free for rendering and input handling.

Reducing the impact of heavy logic processing on frame rates

Tasks to offload to Web Workers:

  • Pathfinding algorithms
  • AI decision making
  • Terrain generation
  • Particle system updates
  • Network communication

Even with React Game Kit or other modern frameworks, keeping these tasks separate improves performance.

Optimizing Rendering Techniques

Clearing and redrawing only necessary parts of the screen

Update only what changes:

// Track dirty regions
const dirtyRegions = [];

function markDirty(x, y, width, height) {
  // Expand by 1px to avoid edge artifacts
  dirtyRegions.push({
    x: x - 1,
    y: y - 1,
    width: width + 2,
    height: height + 2
  });
}

function render() {
  const ctx = canvas.getContext('2d');

  // Clear and redraw only dirty regions
  for (const region of dirtyRegions) {
    ctx.clearRect(region.x, region.y, region.width, region.height);

    // Find entities that intersect this region
    const visibleEntities = entities.filter(e => 
      entitiesOverlap(e, region)
    );

    // Redraw entities in this region
    for (const entity of visibleEntities) {
      drawEntity(ctx, entity);
    }
  }

  // Reset dirty regions
  dirtyRegions.length = 0;
}

This works well for games with limited movement or turn-based games.

Using batching and caching for efficient rendering

Cache rendered elements that don’t change:

// Pre-render each tile type once
const tileCache = {};

function createTileCache() {
  for (const tileType in tileDefinitions) {
    const cacheCanvas = document.createElement('canvas');
    cacheCanvas.width = TILE_SIZE;
    cacheCanvas.height = TILE_SIZE;

    const ctx = cacheCanvas.getContext('2d');
    drawTile(ctx, tileDefinitions[tileType]);

    tileCache[tileType] = cacheCanvas;
  }
}

function renderMap() {
  for (let y = 0; y < mapHeight; y++) {
    for (let x = 0; x < mapWidth; x++) {
      const tileType = map[y][x];
      ctx.drawImage(
        tileCache[tileType],
        x * TILE_SIZE,
        y * TILE_SIZE
      );
    }
  }
}

This dramatically reduces draw calls and improves performance.

Starting, Stopping, and Managing the Game Loop

Controlling the Execution of the Loop

Implementing start and stop functions

Clean control over your game loop makes development easier:

let isRunning = false;
let lastTimestamp = 0;
let animFrameId = null;

function startGameLoop() {
  if (!isRunning) {
    isRunning = true;
    lastTimestamp = performance.now();
    animFrameId = requestAnimationFrame(gameLoop);
    console.log("Game loop started");
  }
}

function stopGameLoop() {
  if (isRunning) {
    cancelAnimationFrame(animFrameId);
    isRunning = false;
    animFrameId = null;
    console.log("Game loop stopped");
  }
}

This pattern keeps track of the animation frame ID, which helps prevent memory leaks.

Frameworks like Babylon.js and PixiJS already implement this functionality in their core engines.

Handling pausing and resuming efficiently

Pausing needs special treatment to avoid time jumps:

let isPaused = false;

function togglePause() {
  isPaused = !isPaused;

  if (isPaused) {
    // Maybe show pause menu
    showPauseMenu();
  } else {
    // When resuming, reset timestamp to avoid time jumps
    lastTimestamp = performance.now();
    requestAnimationFrame(gameLoop);
    hidePauseMenu();
  }
}

function gameLoop(timestamp) {
  if (isPaused) return;

  const deltaTime = (timestamp - lastTimestamp) / 1000;
  lastTimestamp = timestamp;

  updateGame(deltaTime);
  renderGame();

  requestAnimationFrame(gameLoop);
}

Browser rendering engines pause requestAnimationFrame when tabs are inactive, so you don’t need to handle tab switching manually.

Preventing the “Spiral of Death” Issue

Understanding what causes update spirals

The “death spiral” happens when:

  1. Your game can’t keep up with the fixed timestep
  2. The time accumulator grows larger each frame
  3. The loop tries to run more and more updates to catch up
  4. Everything freezes as the browser gets stuck

Games with heavy physics particularly suffer from this issue on low-end devices.

Implementing sanity checks to prevent performance crashes

Always include safety checks in your loop:

const FIXED_DT = 1/60; // 60 updates per second
const MAX_STEPS = 5;   // Never run more than 5 physics updates per frame
let accumulator = 0;

function gameLoop(timestamp) {
  // Calculate real delta time
  let deltaTime = (timestamp - lastTimestamp) / 1000;
  lastTimestamp = timestamp;

  // Cap delta time if the game was inactive
  if (deltaTime > 0.25) deltaTime = 0.25;

  accumulator += deltaTime;
  let updateCount = 0;

  // Run fixed updates with a safety limit
  while (accumulator >= FIXED_DT && updateCount < MAX_STEPS) {
    updatePhysics(FIXED_DT);
    accumulator -= FIXED_DT;
    updateCount++;
  }

  // If we hit the limit, discard remaining time
  if (updateCount >= MAX_STEPS) {
    console.warn("Update spiral detected, dropping accumulated time");
    accumulator = 0;
  }

  // Render with interpolation for smoothness
  const alpha = accumulator / FIXED_DT;
  render(alpha);

  requestAnimationFrame(gameLoop);
}

This approach prevents browser crashes even under heavy load. Popular libraries like React Game Kit implement similar safeguards.

Managing State and Transitions

Initializing the game loop properly

Set up everything before starting the loop:

function initGame() {
  // Load assets
  const assetsPromise = loadAssets([
    'player.png',
    'enemies.png',
    'background.jpg',
    'sounds.mp3'
  ]);

  // Show loading screen
  showLoadingScreen();

  // Wait for assets to load
  assetsPromise.then(() => {
    // Initialize game state
    gameState = createInitialState();

    // Set up input
    setupInputHandlers();

    // Hide loading screen
    hideLoadingScreen();

    // Start the game loop
    lastTimestamp = performance.now();
    requestAnimationFrame(gameLoop);
  });
}

// Start when the page loads
window.addEventListener('load', initGame);

This ensures your HTML5 game won’t start until everything is ready. The CreateJS library uses a similar approach for asset loading.

Handling scene transitions and state changes

Structured scene management keeps your code organized:

const SCENES = {
  MAIN_MENU: 'mainMenu',
  GAMEPLAY: 'gameplay',
  PAUSE: 'pause',
  GAME_OVER: 'gameOver'
};

let currentScene = SCENES.MAIN_MENU;
let sceneTransition = {
  active: false,
  from: null,
  to: null,
  progress: 0,
  duration: 1.0 // seconds
};

function updateGame(deltaTime) {
  // Handle scene transition if active
  if (sceneTransition.active) {
    sceneTransition.progress += deltaTime / sceneTransition.duration;

    if (sceneTransition.progress >= 1.0) {
      // Transition complete
      currentScene = sceneTransition.to;
      sceneTransition.active = false;
      initScene(currentScene);
    }

    return;
  }

  // Update current scene
  switch (currentScene) {
    case SCENES.MAIN_MENU:
      updateMainMenu(deltaTime);
      break;
    case SCENES.GAMEPLAY:
      updateGameplay(deltaTime);
      break;
    // Other scenes...
  }
}

function changeScene(newScene, transitionDuration = 1.0) {
  if (currentScene !== newScene && !sceneTransition.active) {
    sceneTransition = {
      active: true,
      from: currentScene,
      to: newScene,
      progress: 0,
      duration: transitionDuration
    };
  }
}

This approach allows for smooth transitions between game states. WebGL games built with Three.js often use similar state management.

Frequently Asked Questions About JavaScript Game Loops

How do I achieve consistent physics across different devices?

Time-based movement vs. frame-based movement

Always use time-based updates instead of frame counting:

// BAD: Frame-based (speed varies with frame rate)
function updateBad() {
  player.x += 5; // 5 pixels per frame
}

// GOOD: Time-based (consistent regardless of frame rate)
function updateGood(deltaTime) {
  player.x += 300 * deltaTime; // 300 pixels per second
}

This prevents physics from running faster on high-end devices and slower on budget phones.

Dealing with variable performance

Handle performance variations with these techniques:

  1. Fixed timestep physics (as shown above)
  2. Interpolation for smooth rendering
  3. Dynamic quality settings

Modern game development frameworks like Phaser.js handle most of this automatically.

How can I optimize my game loop for mobile browsers?

Battery-friendly techniques

Mobile browsers need special attention:

  • Use requestAnimationFrame (automatically pauses when tab inactive)
  • Reduce update frequency for background elements
  • Implement quality settings based on device capability
  • Use touch events instead of mouse events
// Detect low-power mode or low battery
function shouldReduceQuality() {
  // Check if battery API is available
  if ('getBattery' in navigator) {
    navigator.getBattery().then(battery => {
      if (battery.level < 0.2 && !battery.charging) {
        return true;
      }
    });
  }

  // Check if device is low-end
  const isLowEndDevice = 
    navigator.hardwareConcurrency < 4 || 
    navigator.deviceMemory < 4;

  return isLowEndDevice;
}

// Adjust quality based on device
if (shouldReduceQuality()) {
  enableLowPowerMode();
}

Progressive Web Apps that include games often implement these battery-saving features.

Handling touch input differences

Touch needs different handling than mouse:

// Handle both mouse and touch
let inputX = 0;
let inputY = 0;
let isPointerDown = false;

// Mouse events
canvas.addEventListener('mousedown', e => {
  isPointerDown = true;
  updatePointerPosition(e.clientX, e.clientY);
});

canvas.addEventListener('mousemove', e => {
  if (isPointerDown) {
    updatePointerPosition(e.clientX, e.clientY);
  }
});

canvas.addEventListener('mouseup', () => {
  isPointerDown = false;
});

// Touch events
canvas.addEventListener('touchstart', e => {
  isPointerDown = true;
  updatePointerPosition(e.touches[0].clientX, e.touches[0].clientY);
  e.preventDefault(); // Prevent scrolling
});

canvas.addEventListener('touchmove', e => {
  updatePointerPosition(e.touches[0].clientX, e.touches[0].clientY);
  e.preventDefault();
});

canvas.addEventListener('touchend', () => {
  isPointerDown = false;
});

function updatePointerPosition(clientX, clientY) {
  const rect = canvas.getBoundingClientRect();
  inputX = clientX - rect.left;
  inputY = clientY - rect.top;
}

Libraries like Kontra.js handle input normalization automatically.

Is requestAnimationFrame supported in all browsers?

Browser compatibility

Support for requestAnimationFrame is now universal in modern browsers, but older browsers might need a polyfill:

// requestAnimationFrame polyfill
if (!window.requestAnimationFrame) {
  window.requestAnimationFrame = (function() {
    return window.webkitRequestAnimationFrame ||
      window.mozRequestAnimationFrame ||
      window.oRequestAnimationFrame ||
      window.msRequestAnimationFrame ||
      function(callback) {
        window.setTimeout(callback, 1000 / 60);
      };
  })();
}

The Mozilla Developer Network documentation confirms excellent support across all modern browsers.

Alternative approaches for legacy support

For very old browsers:

// Fallback timing system
class GameTimer {
  constructor(targetFPS = 60) {
    this.targetFPS = targetFPS;
    this.lastTime = 0;
    this.isRunning = false;
    this.useRAF = !!window.requestAnimationFrame;
  }

  start(loopFn) {
    this.isRunning = true;
    this.lastTime = performance.now();

    if (this.useRAF) {
      const rafLoop = (timestamp) => {
        if (!this.isRunning) return;

        const deltaTime = timestamp - this.lastTime;
        this.lastTime = timestamp;

        loopFn(deltaTime / 1000);
        requestAnimationFrame(rafLoop);
      };

      requestAnimationFrame(rafLoop);
    } else {
      // Fallback to setInterval
      const intervalMs = 1000 / this.targetFPS;

      this.intervalId = setInterval(() => {
        const now = performance.now();
        const deltaTime = now - this.lastTime;
        this.lastTime = now;

        loopFn(deltaTime / 1000);
      }, intervalMs);
    }
  }

  stop() {
    this.isRunning = false;

    if (!this.useRAF && this.intervalId) {
      clearInterval(this.intervalId);
    }
  }
}

However, most indie game development now focuses on modern browsers where requestAnimationFrame is well-supported.

How do I handle resizing and fullscreen in my game loop?

Responsive canvas sizing

Keep your canvas properly sized:

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

function resizeCanvas() {
  // Get the display size
  const displayWidth = window.innerWidth;
  const displayHeight = window.innerHeight;

  // Check if canvas size needs to change
  if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
    // Set canvas size to match display
    canvas.width = displayWidth;
    canvas.height = displayHeight;

    // Update game scale/viewport
    updateGameViewport(displayWidth, displayHeight);
  }
}

function updateGameViewport(width, height) {
  // Calculate game scale to maintain aspect ratio
  const gameAspect = GAME_WIDTH / GAME_HEIGHT;
  const screenAspect = width / height;

  if (screenAspect > gameAspect) {
    // Screen is wider than game
    gameScale = height / GAME_HEIGHT;
    gameOffsetX = (width - GAME_WIDTH * gameScale) / 2;
    gameOffsetY = 0;
  } else {
    // Screen is taller than game
    gameScale = width / GAME_WIDTH;
    gameOffsetX = 0;
    gameOffsetY = (height - GAME_HEIGHT * gameScale) / 2;
  }
}

// Listen for resize events
window.addEventListener('resize', resizeCanvas);

// Initial sizing
resizeCanvas();

This approach keeps your game properly scaled regardless of screen size.

Handling fullscreen mode

Add fullscreen support:

const fullscreenButton = document.getElementById('fullscreenButton');

fullscreenButton.addEventListener('click', toggleFullscreen);

function toggleFullscreen() {
  if (!document.fullscreenElement) {
    // Enter fullscreen
    if (canvas.requestFullscreen) {
      canvas.requestFullscreen();
    } else if (canvas.webkitRequestFullscreen) {
      canvas.webkitRequestFullscreen();
    } else if (canvas.msRequestFullscreen) {
      canvas.msRequestFullscreen();
    }
  } else {
    // Exit fullscreen
    if (document.exitFullscreen) {
      document.exitFullscreen();
    } else if (document.webkitExitFullscreen) {
      document.webkitExitFullscreen();
    } else if (document.msExitFullscreen) {
      document.msExitFullscreen();
    }
  }
}

// Handle fullscreen changes
document.addEventListener('fullscreenchange', resizeCanvas);
document.addEventListener('webkitfullscreenchange', resizeCanvas);
document.addEventListener('mozfullscreenchange', resizeCanvas);
document.addEventListener('MSFullscreenChange', resizeCanvas);

Libraries like Babylon.js include these features out of the box for WebGL applications.

Conclusion

Wrapping up, what is JavaScript game loop brings us to the core function that keeps games ticking smoothly. It synchronizes state updates and graphics rendering, creating seamless interactions. Understanding this helps us, as developers, to tackle challenges like CPU load and frame rate consistency, which are critical for real-time applications.

By mastering components like requestAnimationFrame and maintaining stable loop cycles, we push web games to new performance levels. This approach isn’t just about code—it’s about crafting engaging user experiences. With these insights, building dynamic, browser-based games becomes not only feasible but truly exciting.

The JavaScript game loop is a powerful tool in our web development toolkit. As technologies evolve, keeping up with performance techniques will remain crucial. Exploring these methods helps ensure the games we design are responsive, efficient, and ready for the future. Stay curious and keep coding!

Author

Bogdan Sandu is the principal designer and editor of this website. He specializes in web and graphic design, focusing on creating user-friendly websites, innovative UI kits, and unique fonts.Many of his resources are available on various design marketplaces. Over the years, he's worked with a range of clients and contributed to design publications like Designmodo, WebDesignerDepot, and Speckyboy among others.