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:
- CPU processing power
- Browser JavaScript engine efficiency (V8 Engine or others)
- Display refresh rate limits
- 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:
- The event loop gets stuck
- DOM manipulation can’t occur
- User inputs are never processed
- 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

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:
- Natural synchronization with display refresh rates
- Reduced CPU usage when the game isn’t visible
- Better battery life on mobile devices
- Consistent timing between frame updates
- 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:
- Only redraw elements that changed
- Use layers for static elements
- Implement object culling (don’t render off-screen)
- 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

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:
- 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);
- Using a countdown for critical timing
// For timed events that must be precise timer -= deltaTime; if (timer <= 0) { fireEvent(); timer = EVENT_INTERVAL; }
- 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:
- Your game can’t keep up with the fixed timestep
- The time accumulator grows larger each frame
- The loop tries to run more and more updates to catch up
- 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:
- Fixed timestep physics (as shown above)
- Interpolation for smooth rendering
- 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!