Game Rig

Home/Blog/Frame budget

The 16.6 ms frame budget

A frame is a deadline with a fixed end, and most of the deadline is spent before your first line of update code runs.

A game loop looks like it owns the whole frame. It does not. At 60 Hz the screen asks for a new picture every 16.6 ms, and the browser spends part of that window on its own work before any game code runs: recalculating styles, laying out the page, painting, and handing the finished layer to the compositor. Google's rendering guidance puts the number for everything you control at 10 ms, and of the figures in this piece that is the only one I did not work out myself.

The gap between 16.6 and 10 is why a game can be smooth in isolation and stutter on a page. Every game on this site shares its page with a header, a footer, two web fonts, and a strip of HUD elements whose text changes every frame. Frame budget belongs to the page, not to the loop.

I found this the slow way, with Block Stack — a game whose whole simulation is a few dozen rectangles moving one pixel down. It ran beautifully until it was dropped into the real page template, where the header's dropdown menus and the two webfonts turned a 2 ms frame into a 14 ms frame. Nothing about the game had changed.

The same ten milliseconds, at every refresh rate

A frame period is just 1000 divided by the refresh rate, so the budget is not a constant — it shrinks as screens get faster. The last column below subtracts the 6.6 ms of browser overhead that Google's 60 Hz number implies, which is arithmetic on that one published figure rather than a measurement of your machine.

Frame period and the time left for game code, by refresh rate
Refresh rateFrame periodLeft for everything you control
60 Hz16.6 ms10.0 ms
90 Hz11.1 ms4.5 ms
120 Hz8.3 ms1.7 ms
144 Hz6.9 ms0.3 ms

The bottom row is the one that changes how you think about this. A 144 Hz machine does not hand a game a bigger window, it hands it a smaller one and asks for more than twice as many finished pictures inside it. Code that comfortably makes 60 can be visibly short of breath at 144, unchanged.

The other consequence is about what "60 FPS" means. On a 144 Hz screen a game reporting 60 FPS is spending 6.9 ms working and 5.7 ms waiting, which is a completely different situation from spending 16.4 ms and scraping in. Frame rate cannot tell those two apart, which is why the number worth counting is frame time in milliseconds.

What is actually inside the ten milliseconds

Four things share it, and they fail in four different ways.

Input. Key events arrive whenever the browser decides to run them, which is not necessarily once per frame. Reading them into a small state object in the handler and then looking at that object once at the top of the frame keeps the ordering honest and costs nothing.

Simulation. Movement, collision, spawning, scoring. This is the part everyone optimises first and the part that is usually already fast. If the simulation is genuinely the bottleneck, the problem is almost always an allocation per frame rather than arithmetic. The tick itself is discussed in the piece on fixed timesteps.

Drawing. Canvas 2D cost scales with the number of operations, not with how complicated the scene looks. A thousand small fills beat a hundred large gradients, and on a high-DPI screen every one of those fills covers four times the pixels — the reason backing-store size belongs in the same conversation.

The DOM. This is the one that catches people. Writing a score into hudScore.textContent on every frame invalidates the element's layout, and the browser then has to recalculate the geometry of a page that also contains a header, a footer and a canvas. The fix is mundane: only write the string when its value changes, and keep the HUD out of the layout path with an absolutely positioned overlay.

Finding out which of the four you blew

Chrome's performance panel will tell you, event by event, and it is the right tool for a mystery. It is the wrong tool for a number you want on every run, because it needs a person to open it and read it.

What works better here is four counters and a mean. Take performance.now() once at the top of the frame, again after input and simulation, again after the draw calls, and once more at the end. Subtract, keep a moving average over the last 60 frames, and draw the four numbers into the corner of the canvas behind a key. They cost about a microsecond each and they answer the question directly: input, simulation, drawing, or everything that is not the game.

When the fourth number is the large one — the time from the end of the frame to the start of the next — the browser is the one spending it, and no amount of work inside the loop will help. That means the page around the game needs attention, and it is worth saying plainly that this is the case most people meet first.

Pros

  • Four performance.now() calls find the guilty stage in one run, without a profiler or a second window.
  • The numbers survive into production, so a report of "it stutters on my laptop" arrives with a breakdown attached.
  • Frame time in milliseconds is comparable between a 60 Hz and a 144 Hz machine; frame rate is not.

Cons

  • A moving average hides the single bad frame that a player actually noticed, so the worst frame in the window matters as much as the mean.
  • Instrumentation inside the loop is measured overhead, small but not zero, and it has to be switched off before the numbers mean anything at the margins.
  • It tells you which stage is slow and never why, which is exactly the question a profiler answers in ten seconds.

Sources

  • Rendering performance — Google's pixel pipeline, the 16.66 ms frame period, and the 10 ms figure for everything under your control.
  • MDN: requestAnimationFrame — when the callback runs relative to the browser's own paint work, and what the timestamp argument means.
  • MDN: performance.now() — the clock the four counters above are built on, and its resolution limits.