The game only moved once a second, and only on iPad
Oct 25, 2026 Β· Tech Β· νκ΅μ΄
I built Cloud Hop and tested it thoroughly in desktop Chrome. Then I opened it on an iPad, and the little yellow chick moved about once a second. The same code ran at a smooth 60fps in Chrome. Fixing it took three attempts, and the second one went in the wrong direction.
The game draws to an HTML <canvas> element with the 2D drawing API. Chrome and Safari run the same code, but their internals differ. Every browser on iPhone and iPad uses the rendering engine behind Safari (WebKit), so being fast in Chrome says nothing about iPad.
When there is no real iPad at hand, you can imitate one with the iOS simulator that ships with Xcode, Apple's developer tool for the Mac. Safari inside the simulator uses the same engine as a real device, but the drawing is done by the Mac's graphics chip, so the speed can differ from a real device. That difference is the point of this post.
In the simulator's Safari, Cloud Hop ran at 11 to 14fps. The game logic was fine, but drawing a single frame took a long time. On the real iPad the chick moved about once a second, which is unplayable.
Since this would never show up in Chrome alone, I first added a ?debug=fps URL option that prints the frames per second on the board. Seeing a number made it possible to compare where it was slow.
The background has to look continuous out to the edges of the screen. To do that, the shared helper (view.stretch()) read the edge rows of the canvas every frame and stretched them outward, copying the canvas onto itself four times with drawImage. A simplified version looks like this.
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d'); // 2D drawing context
// reads the canvas that was already drawn and draws it onto itself
ctx.drawImage(canvas, 0, 0, width, 1, 0, 0, width, edge);
In Chrome that copy costs almost nothing. In Safari it was very slow.
First I removed the stretching call from Cloud Hop and painted the sky directly over the whole visible area. The art stayed the same, and Cloud Hop was fixed.
But several other games used the same helper: Archery, Bubble Shooter, Knife Toss, Penalty Kick, Home Run Derby, Zigzag Stairs, Spiral Drop and Timber Chop all had the same problem. Instead of fixing them one by one, I decided to fix the shared helper.
I assumed the amount of copying was the problem. So I changed the helper to copy a single edge row into a small canvas and stretch that. The canvas was no longer copied onto itself. Measured in the simulator, the results looked great: seven of those eight games ran at 60fps and Timber Chop at 52fps. Compared with Cloud Hop's 11 to 14fps, it looked fully solved.
I shipped that change.
11fps again on a real iPad
After the deploy I opened Dash Run on a real iPad and it was still at 11fps. The canvas on that device was 2732Γ1784 pixels. The approach that had no problem in the simulator was just as slow on the real device.
That was when I understood that the problem was not how much I copied but the fact that I read the already-drawn canvas back at all. Copying one row into a small canvas still reads pixels from the finished canvas. I added a temporary switch to turn the stretching off or change how it read, and with stretching off the same device ran at 60fps. The simulator uses the Mac's graphics chip, which hid this read cost.
So I removed the shared stretching helper and made each game paint its background directly out to the edges of the visible area. Clouds and enemies are drawn out to that edge as well. No code that reads the drawn canvas back remained.
// simplified example: paint the sky over the whole visible area, never read the canvas back
ctx.fillStyle = sky;
ctx.fillRect(view.left, view.top, view.right - view.left, view.bottom - view.top);
I changed ten games this way. On the real iPad, Dash Run went from 11fps to 120fps. Timber Chop, Archery and Bubble Shooter also reached 120fps after the same change.
Code that reads an already-drawn canvas back is very slow in Safari. Even if the simulator is fast, don't trust it; check on a real device. This time I trusted the simulator's numbers, shipped, and ran into the same problem a second time.
The next post explains how to build this fps counter yourself. It barely touches the game code and goes all the way to reading the number in the iPad simulator.
You can play the game below.
References
- MDN, CanvasRenderingContext2D.drawImage: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage
- MDN, Optimizing canvas: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas
- Apple, Running your app in Simulator or on a device: https://developer.apple.com/documentation/xcode/running-your-app-in-simulator-or-on-a-device
Games from this post
More posts
- I deployed, and the old page kept showing for an hour Oct 25, 2026
- Why there is always a cloud within reach in Cloud Hop Oct 25, 2026
- Why the crumbling clouds never sit on the path Oct 25, 2026
- I removed the background and a pink edge was left Oct 25, 2026
- Three tries in one day to decide what not to deploy Oct 25, 2026
