← All posts

JS Game Loops: requestAnimationFrame vs Web Workers

Compare requestAnimationFrame and Web Workers for browser game physics loops, frame rates, and silky-smooth indie performance.

If you have ever spent your Tuesday evening staring at a terminal while your meticulously coded HTML5 browser game stutters every time a particle effect goes off, you are not alone. With modern indie browser games pushing graphics and physics harder than ever—spurred on by viral TikTok devlogs and deep-dive technical breakdowns on YouTube—mastering the core game loop is the ultimate rite of passage.

Lately, community debates on GitHub and X (formerly Twitter) have been raging over how to achieve buttery-smooth physics without turning the user's laptop fan into a jet engine. Should you stick to the classic requestAnimationFrame, or offload your heavy math to Web Workers? Let’s dive under the hood and look at how modern JavaScript handles the beating heart of your game.

Entity Definition: What is a JavaScript Game Loop?

Game Loop Architecture is the continuous execution cycle in software engineering that processes user input, updates game state, and renders graphics frame-by-frame, ensuring deterministic time-step progression regardless of host hardware variations.

In plain English, it is the infinite while-loop equivalent that keeps your browser tab from freezing while stopping your physics engine from exploding when a player drags their window across a high-refresh-rate monitor.


The Contenders: rAF vs Web Workers

To understand why physics sometimes drift into slow-motion territory, we need to compare our two main contenders.


[Main Thread]  ---> requestAnimationFrame (UI & Rendering synced to 60Hz/120Hz)
[Background]   ---> Web Workers (Dedicated CPU threads for heavy number crunching)

1. requestAnimationFrame (rAF)

For years, requestAnimationFrame has been the darling of the indie browser game scene. It syncs your game updates with the browser’s native repaint cycle, keeping animations smooth and preventing wasted CPU cycles when a player minimizes your tab.

  • Pros: Inherent synchronisation with the display refresh rate; direct access to the DOM and Canvas API; incredibly straightforward to implement.
  • Cons: Tightly bound to the main thread. If your collision detection takes too long, your frame rate drops, input lag spikes, and your players start rage-quitting.

2. Web Workers

Web Workers let you run JavaScript code in background threads, completely isolated from the main UI thread. They communicate via message passing (postMessage), making them ideal for heavy lifting like procedural generation, pathfinding, and complex physics integration.

  • Pros: Zero main-thread blocking; true parallel processing on multi-core CPUs; keeps your frame rates silky-smooth even during heavy calculations.
  • Cons: No direct DOM access; serialization overhead when passing large object arrays back and forth via postMessage.

Technical Comparison Table

FeaturerequestAnimationFrameWeb Workers (Dedicated)
Primary ThreadMain UI ThreadBackground Worker Thread
DOM AccessFull AccessNone (Must post messages to main thread)
Refresh Rate SyncNative (Matches monitor Hz)Manual (Requires custom setInterval/timers)
OverheadMinimalLow-to-Moderate (Serialization cost)
Best Used ForRendering, input handling, simple updatesHeavy physics, AI pathfinding, map generation

The Modern Solution: Hybrid Architecture

According to recent consensus among high-performance web game developers on technical forums, the sweet spot for modern indie browser games isn't choosing either requestAnimationFrame or Web Workers—it's using both in a decoupled architecture.

Here is a simplified code snippet showing how you can run a fixed-timestep physics loop inside a Web Worker while keeping your rendering loop snappy on the main thread:


// main.js (Main Thread - Rendering & Input)
const worker = new Worker('physics-worker.js');
let latestState = {};

window.addEventListener('keydown', (e) => {
    worker.postMessage({ type: 'INPUT', key: e.code });
});

worker.onmessage = (event) => {
    latestState = event.data; // Receive calculated positions
};

function renderLoop() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    drawEntities(latestState); // Render whatever the worker gave us last
    requestAnimationFrame(renderLoop);
}

requestAnimationFrame(renderLoop);

// physics-worker.js (Background Thread - Uncapped Physics)
let gameState = { playerX: 0, velocityY: 0 };
const FIXED_TIMESTEP = 1000 / 60; // 60 updates per second

setInterval(() => {
    // Run heavy physics math here away from the render thread
    gameState.playerX += gameState.velocityY;
    postMessage(gameState);
}, FIXED_TIMESTEP);

Key Takeaways for Indie Devs

  • Decouple physics from rendering: Never let a frame drop slow down your game simulation speed. Use fixed timesteps.
  • Profile before optimizing: Don't spin up Web Workers for simple arcade games with three moving squares. Use them only when your main thread starts sweating.
  • Mind the data bridge: Passing massive arrays of complex classes through postMessage creates garbage collection pressure. Keep your state structures flat and lean.

Whether you are building the next viral web puzzle hit or a frantic retro shooter, nailing your loop architecture ensures your game plays just as smoothly on a budget smartphone as it does on a high-end rig.

Thanks for reading. Browse more from the Wobblox blog, or jump straight into all 100 free games.