For most of the web's history, a physics game in a browser meant a plugin. JavaScript was too slow, there was no good drawing surface, and there was no reliable way to synchronise animation with the display. All three of those problems were solved within a few years of each other, and browser gaming changed completely.

This guide covers what the web platform actually provides, what it costs, and the specific techniques that make a physics simulation viable inside a tab that is also running a browser.

The three things that made it possible

A fast JavaScript engine

Modern JavaScript engines compile hot code to machine code at runtime. Numeric loops — exactly what a physics engine is — benefit most, and a well-written integrator now runs within a small factor of equivalent native code.

The practical consequence is that a physics engine no longer needs to be written in another language and compiled. Plain JavaScript is fast enough for hundreds of bodies.

A drawing surface

The canvas element gives a page a bitmap you can draw into imperatively, and browsers back it with the GPU. Drawing a thousand shapes per frame is routine; the cost is in the number of state changes and the number of pixels filled, not in the shapes themselves.

A frame callback

requestAnimationFrame asks the browser to call you just before the next repaint. It matches the display refresh rate, it stops entirely when the tab is hidden, and it provides a high-resolution timestamp. Before it existed, games used setInterval and fought the compositor constantly.

Why timers are not enough

A setInterval callback fires on a schedule that has nothing to do with when the screen actually updates, so frames get drawn twice or not at all, producing judder. It also keeps running in a hidden tab, burning battery for nothing.

Try it: Particle Push

Herd a swarm of light particles into a pen using a single repulsor field.

Play Particle Push

The loop in practice

A browser physics game's loop has to reconcile two different clocks: the display's refresh rate, which varies by device, and the simulation's step rate, which must not.

function frame(timestamp) {
  requestAnimationFrame(frame);
  let dt = (timestamp - last) / 1000;
  last = timestamp;
  if (dt > 0.1) dt = 0.1;      // discard backlog
  accumulator += dt;
  while (accumulator >= STEP && steps < MAX) {
    simulate(STEP);
    accumulator -= STEP;
  }
  render();
}

Three details in that snippet matter more than they look.

  • requestAnimationFrame is called first. Scheduling the next frame before doing any work means an exception in the body does not stop the loop forever.
  • The delta is clamped. A backgrounded tab returns a delta of several seconds. Simulating all of it would stall the page and teleport objects through walls.
  • The step count is capped. Without a cap, a device that cannot keep up falls further behind every frame until it freezes — the spiral of death.

What rendering actually costs

Canvas performance is not intuitive. The expensive operations are not the ones that look expensive.

OperationRelative costNotes
Filling a large areaHighCost scales with pixels touched
Changing fillStyleModerateState changes break batching
save() / restore()ModerateCheap individually, adds up in a loop
Shadows (shadowBlur)Very highOften the single biggest cost
Drawing a pathLowShape complexity matters little
Text renderingHighCache it if it does not change

The practical rules follow directly: group draws by colour, avoid shadows except on a handful of hero objects, do not call save()/restore() inside a per-particle loop, and never render text that has not changed.

Device pixel ratio

A high-density display reports a ratio of 2, 3 or even 4. Rendering at native resolution means filling four, nine or sixteen times as many pixels. Capping at 2 is almost universal, because the difference above that is imperceptible on a moving image and the cost is enormous.

The most common canvas mistake

Setting the canvas width and height attributes on every frame. That reallocates the backing store and clears it — an expensive operation that is easy to trigger accidentally from a resize handler that is not debounced.

Memory, garbage and stutter

JavaScript's garbage collector is the single most common cause of inconsistent frame times in a browser game, and it is entirely avoidable.

Every object allocated inside the game loop eventually becomes garbage. When enough accumulates, the collector runs, and that pause is a dropped frame — visible as a stutter.

The fixes

  1. Reuse vectors. A vector class with mutating methods lets you write a.add(b) instead of a = a.plus(b), allocating nothing.
  2. Pool short-lived objects. Particles are the classic case: allocate a fixed pool once, mark entries active or inactive, and never allocate again.
  3. Reuse arrays. Setting array.length = 0 empties an array without allocating a new one.
  4. Avoid closures in hot loops. A function created inside a loop is an allocation.

The particle system on this site allocates its entire pool up front and recycles the oldest entry when saturated. It never allocates during play, which is why a screen full of debris does not cause a stutter three seconds later.

Loading only what is needed

A physics engine is dead weight on a page with no game on it. Loading it everywhere costs every visitor bandwidth and parse time for nothing.

The approach here is per-page script inclusion: the fourteen physics modules and the game shell are included only on individual game pages. A guide, a category page or the home page loads none of them.

Why classic scripts rather than modules

ES modules are the modern default, and they have one practical drawback for a project like this: they are subject to CORS, which means a page opened directly from the filesystem cannot load them. Classic scripts attached to a namespace work from file://, from a local server and from production identically.

For a static site that people may download and open locally, that is worth more than the module system's advantages. The trade-off is manual dependency ordering, which a build step enforces.

Deferred by default

Every script on this site uses defer, so nothing blocks the parser and execution happens in document order after parsing. The page renders before any JavaScript runs, which is what keeps the largest contentful paint fast.

Saving state without a server

A static site has no database, but browsers have localStorage — a small synchronous key-value store scoped to the origin.

It is enough for everything a single-player arcade site needs: high scores, favourites, play history, settings. It is also genuinely private, because the data never leaves the device.

What goes wrong

  • It throws. In private windows and when site data is blocked, even reading throws an exception. Every access needs a try/catch.
  • It fills up. The quota is typically 5–10 MB per origin. Writing past it throws a QuotaExceededError.
  • It is synchronous. A large read or write blocks the main thread, so keep entries small.
  • It can be corrupted. Anything hand-edited or written by an older version may fail to parse. Treat every read as untrusted.

The storage layer here wraps all of that: guarded access, an in-memory fallback when storage is unavailable, corrupted-entry recovery, and quota handling that trims play history before giving up.

Performance beyond the frame rate

A game page is still a web page, and it is measured as one. The three metrics that matter are worth understanding because they constrain design decisions.

  • Largest Contentful Paint — how quickly the main content appears. Deferred scripts and no render-blocking resources are the main levers.
  • Cumulative Layout Shift — how much the page jumps around while loading. Explicit width and height on every image, and a reserved aspect ratio for the canvas, prevent almost all of it.
  • Interaction to Next Paint — how quickly the page responds to input. For a game page this is mostly about not doing heavy work on the main thread before the first interaction.

Ad slots deserve specific mention: an ad that loads and pushes content down is a layout shift. Reserving the slot's height in CSS before anything loads is the fix, and it is why every ad container on this site has a declared minimum height.

Try it: Physics Arcade Ultimate

Six zones, one run: gravity, bounce, balance, motion, collision and force.

Play Physics Arcade Ultimate

Debugging a physics game in a browser

The browser is an unusually good environment for debugging physics, because the tools are already there and the code is not compiled away.

Draw the invisible

The single most useful technique is rendering what the engine is actually working with: bounding boxes, contact points, contact normals, velocity vectors, the broad-phase grid. Nine times out of ten a bug that was mysterious becomes obvious the moment you can see the normals.

The engine here has a drawDebug method that overlays all of that plus live counters for bodies, candidate pairs, contacts and steps per frame. Those four numbers diagnose most performance problems on their own.

Step the simulation manually

Physics bugs often happen in a single frame and are invisible at full speed. Being able to advance one step at a time — and to run at a tenth speed — turns 'it sometimes explodes' into a reproducible observation.

Use the performance profiler

A browser's built-in profiler shows exactly where frame time is going. In a physics game the answer is almost always one of three things: the narrow phase, canvas fill operations, or garbage collection. Each has a different fix, and guessing between them wastes days.

Symptom in the profilerLikely causeFix
Long scripting blocks, steadyNarrow phase or solver costBetter broad phase, fewer iterations
Long painting blocksFill cost, shadows, high DPRCap DPR, remove shadowBlur
Regular spikes every few secondsGarbage collectionPool objects, reuse vectors
One enormous frame after a pauseUn-clamped deltaClamp frame time, cap steps

Test on the slow device, not the fast one

A physics game that runs at 60fps on a desktop may run at 20 on a three-year-old phone, and the fixed-timestep accumulator will be taking its maximum steps every frame to compensate. That is the condition worth testing, because it is where most players actually are.

Key takeaways

  • Browser physics became viable when fast JavaScript, canvas and requestAnimationFrame arrived together.
  • Always schedule the next frame first, clamp the delta, and cap the step count.
  • Canvas cost is dominated by pixels filled, state changes and shadows — not by shape complexity.
  • Cap device pixel ratio at 2; above that you pay for detail nobody can see.
  • Garbage collection pauses are visible stutters — pool objects and reuse vectors.
  • Load the physics engine only on pages that have a game on them.

Frequently asked questions

Can browsers really run physics simulations smoothly?

Yes. Modern JavaScript engines compile numeric loops to machine code, and a well-written engine handles hundreds of bodies at 60fps in a tab, including on mid-range phones.

Why do browser games use requestAnimationFrame instead of setInterval?

Because it synchronises with the display refresh, provides a high-resolution timestamp, and stops entirely when the tab is hidden. Timer-based loops produce judder and waste battery in background tabs.

What causes stuttering in browser games?

Most often garbage collection. Objects allocated inside the game loop accumulate until the collector runs, and that pause is a dropped frame. Pooling and vector reuse eliminate it.

Why cap the device pixel ratio?

A ratio of 3 means nine times as many pixels to fill for a difference that is imperceptible on a moving image. Capping at 2 typically recovers more than half the rendering budget on a high-density phone.

Do browser games work offline?

A purely static game will work once cached, and a service worker can make that reliable. The games here need no server at all once loaded, since all state is kept in localStorage.

Why use classic scripts instead of ES modules?

Modules are subject to CORS, so a page opened directly from the filesystem cannot load them. Classic scripts on a namespace work identically from file://, a local server and production.

References

Physical concepts in this guide were checked against the following educational sources. None of them are affiliated with Physics Arcade.

  1. MDN Web Docs — Canvas API
  2. MDN Web Docs — window.requestAnimationFrame()
  3. MDN Web Docs — Pointer events
  4. web.dev — Core Web Vitals
  5. Glenn Fiedler — Fix Your Timestep!