HTML5 game publishing

Holding 60 FPS in Mobile Browsers: Frame Budget, Garbage Collection and Texture Memory

Where the 16.7 ms goes, why random hitches appear every few seconds, and how texture size becomes a memory problem long before it becomes a speed problem.

PlayKovo Team9 min read
Games currently available on PlayKovo. Open a title to see its current listing.

Our guide on load time covers how fast a game opens. This one is about what happens after it opens: whether the tenth minute of play feels like the first. On a phone that depends on three things you control, the frame budget, the garbage collector and texture memory.

The frame budget, in numbers

At 60 frames per second each frame has 16.7 ms. The browser needs part of that for its own work, so your update, physics, rendering calls and any other script realistically get 10 to 12 ms. On a phone chip that is several times slower than a laptop, work that took 2 ms on your machine can take 8 ms there. A budget that felt generous suddenly is not.

Keep the main thread clear of anything that is not needed every frame. Run AI decisions every few frames rather than every frame, update off-screen objects less often, and move heavy one-off work such as level generation to a Web Worker or spread it across several frames. Use requestAnimationFrame for the loop and calculate movement from elapsed time, so a dropped frame makes the game look choppy but not slow.

Hitches every few seconds: garbage collection

If the game is smooth and then freezes for a moment every five or ten seconds, the likely cause is garbage collection. JavaScript frees memory automatically, but it has to stop and sweep up when enough short-lived objects have piled up. A game loop that creates new vectors, arrays, closures or strings every frame produces that pile very quickly.

The Performance panel shows this clearly: a sawtooth in the memory graph that climbs and drops, with a collection event at each drop. The fix is to stop creating the garbage.

  • Pool objects that appear and disappear often, such as bullets, particles, enemies, floating score text and sound instances. Take one from the pool, reset it, return it when done.
  • Reuse temporary vectors and arrays. Allocate scratch objects once outside the loop and overwrite them.
  • Avoid building strings every frame. Update on-screen text only when the value changes.
  • Avoid creating functions inside the loop, for example arrow functions passed to <code>forEach</code> on large arrays each frame. A plain <code>for</code> loop allocates nothing.
  • Be careful with array methods that return new arrays, such as <code>map</code>, <code>filter</code> and <code>slice</code>, in hot code.

You do not need to do this everywhere. Profile first, find the few functions that allocate the most, and fix those.

Texture memory is the quiet crash

File size on disk is not what the GPU cares about. A texture is decompressed into memory at its full pixel size, four bytes per pixel for a standard RGBA image. A 2048 by 2048 texture takes 16 MiB, or about 21 MiB with mipmaps. A 4096 by 4096 texture takes 64 MiB, about 85 MiB with mipmaps. A game with a dozen of those can ask a phone browser for a gigabyte before it has drawn a frame.

Mobile browsers set limits on how much memory a page may use and will kill a tab that goes over, often without a useful error. On iPhones the symptom is a game that reloads itself or goes white after a few levels. Frame rate is not the problem here; the game ran out of memory.

  • Pack many small sprites into texture atlases so they share one texture and one draw call.
  • Size textures for how big they appear on screen. A 2048 pixel image drawn at 200 pixels wide is wasted memory.
  • Consider a compressed GPU format such as KTX2 with Basis Universal where your engine supports it. It stays compressed in video memory, so a texture takes a fraction of the space.
  • Release what you no longer need: dispose textures, geometries and render targets when a level ends, and drop references so they can be collected.
  • Do not decode all audio up front. Long music tracks decoded into memory can use tens of megabytes; stream them instead.

How to see the memory problem

In desktop Chrome, the Memory panel and heap snapshots show what is held. performance.memory is available only in Chromium and is rough, so use it as a trend, not a measurement. For the phone itself, play through three or four levels in a row on a real device and watch whether memory keeps growing. A game that is stable on level one and dies on level five has a leak: something from each level is never released.

Test the way players play

Long sessions reveal more than short ones. Play for 15 minutes, switch to another app and come back, rotate the phone, and let the screen dim. Check that the game still runs, that sound has not doubled and that it pauses when the tab is hidden. These are the failures that make players leave and reviewers reject builds.

When your numbers look right, record them: device model, browser, average frame time and memory after ten minutes. Put the summary in your submission notes. It tells a reviewer you tested on hardware, and it gives you a baseline to compare against after every update.

We use optional cookies for site analytics and site advertising. You can change your choice later. Privacy policy.