HTMLtoApphtmltoapp.net Build free
games

HTML5 game performance on Android: getting a steady 60 FPS

10 min readUpdated 2026-10-05By the HTML to App team

An HTML5 game runs as fast in an Android app as it does in Chrome on the same phone, because both use Chromium with GPU acceleration. To hold 60 FPS on mid-range phones, render with WebGL rather than canvas 2D, cap the canvas resolution instead of using the full devicePixelRatio, pack sprites into texture atlases, avoid allocating objects every frame, and pause the loop when the app is hidden.

The 16.7 ms budget

At 60 FPS each frame gets 16.7 ms for your update logic, rendering and the browser's own work. On a mid-range phone, a canvas the size of the full screen at devicePixelRatio 3 is about 1080×2400 pixels — 2.6 million pixels to fill every frame. Most stutter traces back to that number.

1. Cap the render resolution

const dpr = Math.min(window.devicePixelRatio || 1, 2); // cap at 2 canvas.width = Math.round(innerWidth * dpr); canvas.height = Math.round(innerHeight * dpr); canvas.style.width = innerWidth + 'px'; canvas.style.height = innerHeight + 'px';

Capping DPR at 2 (or 1.5 for pixel-art and fill-heavy games) cuts pixel count by up to 55% with little visible difference on a phone. In Phaser use resolution; in PixiJS resolution: Math.min(devicePixelRatio, 2).

2. WebGL over canvas 2D

Canvas 2D is fine for puzzle and board games with few moving parts. For anything with dozens of sprites, use a WebGL renderer (Phaser AUTO, PixiJS, Three.js): it batches sprites from one texture into a single draw call. Put sprites into texture atlases so batching can happen — 200 sprites from one atlas can be one draw call; from 200 separate PNGs, up to 200.

3. Stop allocating in the loop

Garbage collection pauses show up as a hitch every few seconds. Inside update(), avoid creating arrays, objects, closures and strings. Pool bullets, particles and enemies; reuse vectors. In Chrome DevTools' Performance panel, a sawtooth memory graph means allocation in the loop.

4. Input that feels native

  • Use Pointer Events (pointerdown), not click — click waits for gesture disambiguation.
  • touch-action: none on the canvas so the browser never tries to scroll or zoom.
  • Read input once per frame from a state object rather than doing work inside event handlers.

5. Audio

Chromium starts the AudioContext suspended until a user gesture. Call ctx.resume() in the first pointerdown. Decode sounds up front, keep effects short and mono, and prefer one music track at a time — several decoded tracks can cost tens of MB of memory.

6. Pause when hidden, respect the battery

document.addEventListener('visibilitychange', () => { if (document.hidden) { game.loop.sleep(); audioCtx.suspend(); } else { game.loop.wake(); audioCtx.resume(); } });

requestAnimationFrame already stops when the app is backgrounded, but timers and audio do not. A game that keeps playing music behind the home screen collects one-star reviews.

Measure on a real, cheap phone

Your flagship phone hides problems. Test on a 3–4-year-old mid-range device. Before packaging, profile in Chrome on that phone over USB (chrome://inspect works for Chrome tabs, not for release apps): if it holds 60 FPS there, it will in the APK.

Questions people ask

Is an HTML5 game slower inside an APK than in Chrome?

No. The app's WebView is the same Chromium engine with the same GPU acceleration. Performance is the same on the same phone.

Should I use canvas or WebGL for a mobile HTML5 game?

WebGL for anything with many sprites or effects; canvas 2D is adequate for simple puzzle and board games. Engines like Phaser and PixiJS choose WebGL automatically.

Why does my game stutter every few seconds?

Most often garbage collection from objects allocated every frame. Pool and reuse objects inside the game loop.

Your HTML is one upload away from an app

Install the free builder, drop in your HTML file or ZIP, and download a signed APK in a few minutes. No watermark, no card, no Android Studio.

Build my APK free