HTML6 GamesFree Web Games / devlog

No install. Just play.

Free mini games right in your browser. Sign in to save your records to your account and pick up on any device.

All games
All games
All dev log posts

A ten-line FPS counter for browser games

Oct 25, 2026 Β· Tech Β· ν•œκ΅­μ–΄

The last post covered fixing a game that was slow only on iPad. I could find the cause because the screen showed the frame rate. A feeling that something is slow doesn't tell you whether a fix helped, and you can't compare devices with it. This post explains how I built that counter and what I went through to read it on an iPad.

FPS is how many times the screen was redrawn in one second. In a browser game loop you normally register your draw function with requestAnimationFrame. Right before the browser updates the screen it calls that function and passes in the current time. So you count how many times the function ran, and every time a second has passed you divide the count by the elapsed time.

If your game already has a loop, it takes one extra call inside it. A simplified example:

let frames = 0;
let since = performance.now();
let label = null;

function countFrame(now) {
  frames++;
  if (now - since < 1000) return;

  const fps = (frames * 1000) / (now - since);
  if (!label) {
    label = document.createElement('div');
    label.style.cssText = 'position:absolute;top:6px;left:50%;translate:-50% 0;' +
      'padding:2px 6px;border-radius:6px;background:rgba(0,0,0,.6);color:#fff;font:700 13px system-ui;pointer-events:none';
    document.body.append(label);
  }
  label.textContent = fps.toFixed(1) + 'fps';
  frames = 0;
  since = now;
}

function loop(now) {
  update();
  draw();
  countFrame(now);
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

The reason to wait a second and average is that the value jumps around from frame to frame. Computing it from the gap between two frames makes the number change constantly and impossible to read. The label is created once and only its text changes afterwards. pointer-events: none keeps it from stealing input from the game.

At first I put this code into Cloud Hop only. Then it turned out other games that used the same helper had the same symptom. Instead of copying the code into each game, I moved it into the loop that all games share. A game where it isn't switched on doesn't run a single line of it.

You turn it on by adding ?debug=fps to the address. The fact that it also works on the live site mattered. On an iPad or iPhone you just change the address, no separate build is needed, and the label is read-only so it doesn't affect records or scores.

Next to the fps I also print the canvas size in pixels (width x height). The same screen can have a very different number of real canvas pixels depending on the device, and a high-resolution display draws far more. The real iPad where Dash Run was slow had a 2732Γ—1784 canvas. With that number next to it, you can tell whether the slowness comes from how much is drawn or from how it is drawn. A lowres option, which shrinks the canvas for comparison, can be added to the same address.

What it took to set up the simulator

A real iPad isn't always within reach, so I used the iOS simulator in Xcode. The simulator is a program that imitates an iPad on the Mac, and the Safari inside uses the same engine as the real device. The first cost was downloading the iOS simulator platform, about 8GB. After that you create an iPad device and boot it with a few commands.

xcodebuild -downloadPlatform iOS
xcrun simctl create "iPad A16 test" com.apple.CoreSimulator.SimDeviceType.iPad-A16 com.apple.CoreSimulator.SimRuntime.iOS-26-1
xcrun simctl boot {device id}

xcrun simctl controls the simulator from the terminal. To open an address you use xcrun simctl openurl. The first run prints a timeout error even though Safari does open, which is easy to mistake for a failure. The simulator can reach the development server running on the Mac, so after changing code you only need to reload.

I read the numbers off the simulator screen with screenshots. Once there were nineteen games, opening them one by one became tedious, so I wrote a script that reads the text in the screenshots. macOS has a built-in text recognition feature (Vision) that finds text in images, and the script calls it. For each game it opens the address, takes a screenshot, reads the number and puts everything into a table.

Simulator screenshots always come out as portrait pixels even when the device is rotated to landscape, so the text can end up lying on its side. I made the script read the picture at 0, 90, 180 and 270 degrees. When a game is portrait-only or landscape-only and shows a "please rotate your device" screen, the script rotates the simulator itself and reads again. The first run showed eighteen of nineteen games at 53 to 60fps.

As the last post explained, the simulator uses the Mac's graphics chip. The same game that ran at 60fps in the simulator ran at 11fps on the real iPad. So I use simulator numbers to pick a direction and do the final check on a real device with the same address. The counter works on the live site as well, so after deploying you can add ?debug=fps to the address on an iPad and see the same number.

Once you have an fps counter, a performance problem turns from a feeling into a number. The code is short, but without it finding why the iPad was slow would have taken much longer.

References

Games from this post

Cloud HopBounce from cloud to cloud and climb as high as you can. Gray clouds fall apart, and storm clouds must be stomped from above! Play

More posts

Add to Home Screen

  1. Tap the Share button at the bottom of the screen (top on iPad). It looks like a square with an arrow pointing up.
  2. Scroll down and tap "Add to Home Screen".
  3. Tap "Add" at the top right. The HTML6 Games icon appears on your home screen.

This works in Safari. Opened from the home screen, it runs without the address bar, like an app, and even offline.