The BobSprite Raw Blog


Pixel Art, Programming and Video Games

(click here and go to index)

60 FPS Browser Game: Truth or Lie?

A study on performance of JavaScript 2.5D games

Ultima 8 screenshot
Ultima 8


In this article, we check whether the browser is capable of running a sprite game at 60 FPS (frames per second) or not. We discuss architecture options, run performance tests and discover a HUGE catch.
*      *      *

The Loop

An application running at 60 FPS means that it has less than 16,666 ms (1 second / 60) to complete the execution of each loop. But since the browser and the operating system have other tasks to perform, in practice we have half of this time.

If we want a STRONG margin of safety (we don't know all the circumstances in which our game will run), we may consider 3 ms as the maximum time available for the execution of each loop in the game.

*      *      *

The Browser and The System

The tests for this article were done using Chrome version 85.0 on a Linux notebook.

Why Chrome? Because it is by far the fastest (at the time of writing this article). Therefore, if the code doesn't work in Chrome, we know it is not going to work anywhere else.

Luckily for us Chrome has the lion share in the browsers market. It is free and easy to install. So if our game needs Chrome to run... no big deal!

*      *      *

The Game

The image at the beginning of the article is a good example of the type of game I used as inspiration for the tests. A 2.5D adventure like game, third person view, which the player's avatar is always positioned at the center of the canvas. The game environment is a labyrinth of 100x100 squares with lakes, creatures, and many trees and many walls.

*      *      *

The Game Logic

In a simplistic description, the code for the game mechanics uses 4 lists: for terrain, for creatures, for missiles and for callbacks (it handles what others don't).

The terrain list is the large list, containing 10 thousand (100x100) squareObjects. Each squareObject tells everything about the square of the terrain that it represents, including the creature and the missiles that may be over it.

The creatures list and the missiles list are small. And are used only for efficiency. Every loop, creatures and missiles must be updated. It is more efficient iterate over the 2 small lists than iterate over a 10 thousand object list.

The weight of game logic, even with the AI of creatures working, is negligible. Running the game without painting takes 0.1 ms or less. Therefore, we will no longer discuss the logic of the game in this article.

*      *      *

The Painting

When the avatar walks, his whole environment must be displaced on canvas (since he is always painted in center of canvas). Therefore we must repaint the whole canvas every loop. This is an expensive operation.

We could imagine an alternative, cheap repainting system for when the avatar is still. But its a bad idea for 2 reasons. First, it does not help when avatar walks (the game must be performant 100% of the time). Second, this system would be very hard to implement due to the intricated overlapping of the volume elements (creatures, walls, speaks, missiles, trees, etc..), mainly the moving ones, each with its own shape and size.

The game canvas has 780 pixels as width and 600 pixels as height. This corresponds to 13x10 squares of 60 pixels each side.

*      *      *

The Chrome FPS Meter


60 FPS test
Chrome FPS Meter

The Chrome browser offers precious tools that give information about the running web page. One of them is exactly what we need for our purpose: Chrome FPS Meter.

The current version of the Chrome FPS Meter features a very important advance. It tells how many frames are dropped (not printed on screen). And shows the percentage of the recent printed frames over all recent produced frames. The maximum value it shows is 99% even when all the frames are printed.

Any Chrome tool is prone to decrease the web page performance. So whatever the test says, it would be a bit better without using the Chrome FPS Meter.

Note: the Chrome FPS Meter does not measure performance of a game that runs inside an iframe tag. If the rest of the page is static, the tool tells that no frame was dropped; doesn't matter how bad can be the performance of the iframed game.

*      *      *

The 60FPS Test

The test is done using a simple web page that reflects the average effort to paint the game. It has a 780 x 600 canvas painted with rectangles:

The rectangles, except grass, will move west 1 pixel per loop (60 pixels per second), simulating the walk of the avatar.

After a moment, the canvas will begin to show 4 stats:


You can run the test here.
You can get the code at GitHub.

Note: the test is NOT how we really draw sprites on game canvas. Its code must be simple or else we lose focus.

*      *      *

Analysing the 60FPS Test


60 FPS test
60 FPS test

Above we see a screenshot of the test running. Screenshots taken in other moment would tell similar stats.

We can see that the average time needed to execute the painting (which is the bottleneck) is 1.7 ms. Much lower than the strict limit of 3 ms that we chose. The worst case was 2.3 ms, also bellow the 3 ms! Excellent!!!

Now let's check how the FPS is going. The average is 59.7 and the worst is 59. Excellent! Small imprecisions are normal.

Our internal, JavaScript tests, tell us that we can very easily run a browser game at 60 FPS.

We only need to check what the Chrome FPS Meter says. Oh no! It says that 50% of the frames are being dropped!!! It is like paints one frame and drops the next, paints one frame and drops the next... It is bad!!! And we are not sure if the frame dropping is so regular or it is like paints 1, drops 4, paints 2, drops 1, paints 3, drops 1...

So we believe in our Javascript code or in the Chrome FPS Meter? We can believe both. And more than believe them, believe in the experience we have playing the game: is it smooth or not?

My guess: with our tests written in JavaScript we test the performance INSIDE the V8 runtime. But there is more job outside the V8 runtime, like interact with the video graphics card. This can only be measured with the Chrome PFS Meter. It seems the video card is unable to process everything it receives.

So why dropping exactly 50%? Another guess: Chrome realizes it can't keep running at 60 FPS. So instead of trying it, failing almost all the time, Chrome decides to go directly to 30 FPS, providing a smoother experience.

Chrome was unstable during the test. Sometimes it starts at 60 FPS, then just because I move (not drag) the mouse, it goes down to 30 FPS.

*      *      *

PixiJS, ThreeJS, WebGL, WebAssembly or Just Your Clever JavaScript Code?

If you are thinking on rewriting a game like this in C, C++ or Rust and later convert to WebAssembly to speed up ONLY the game logic, I would tell that is not necessary. JavaScript handles very well the game logic.

I made VERY superficial attempts with PixiJS, WebGL and ThreeJS. I got no better results painting sprites; maybe I did not test them in the right way. I am NOT saying they can’t work fine. I gave up because I preferred to spend my time finding a way to make the game work with pure JavaScript and 2D canvas, instead.

I will talk more at the Conclusion section.

*      *      *

The Heating - Your Worst Enemy


Monitoring the heat
Monitoring the heat

If your Operating System is Linux, type sensors  and [ENTER]  at your terminal. This shows the temperature of the processors in your machine. This is an excellent reliable method to check if your application is demanding too much from them.

When your application stresses the system, the temperature rises. But the processors will not meltdown because the operating system starts a defensive protocol delaying (maybe skipping I can't tell) the execution of processes.

This defensive protocol causes the following weird effect. Your game is running with acceptable performance. Suddenly it starts to perform horribly. Let me be clear, I mean HORRIBLY. One minute later the game returns to acceptable performance. And later it repeats this cycle.

If you are not aware of the heating problem you start looking madly for a memory leak that doesn't exist. You refactor everything and find nothing wrong. Then you give up making your browser game, because you believe browsers are not reliable for games.

*      *      *

Running OK at 30 FPS

Since in practice Chrome paints 30 frames per second, let's accept and cooperate with this inevitable fate.

First we need to adjust the game mechanics (logic) to run at 30 FPS. For example, the avatar speed is no longer 1 pixel per loop. It must be 2 pixels per loop, matching the original 60 pixels per second speed.

Second we must adjust the game loop like in the pseudocode below:

#            
# Running smoothly at 30 FPS 
# without heating the processors
#

function main() {
    init()
    runMainLoop()
}

function runMainLoop() {
    # real execution
    runGameLogic() # walking TWO pixels per loop
    paint()
    requestAnimationFrame(relax)
}

function relax() {
    # pseudo execution
    # does nothing
    # processors may cool down
    # plenty of time for system tasks
    # plenty of time for browser tasks
    requestAnimationFrame(runMainLoop)
}

This system has very important advantages. We stop stressing the browser, OS and machine each loop, giving them time to cool down. The system will not need to fire the heat defense protocol. We have a stable, predictable performance. The change in code is minimal. The game will be jank-free. 30 FPS is not bad for a sprite game.

60 FPS test
30 FPS test

You can run the test here.
You can get the code at GitHub.

*      *      *

Conclusion

This article is my attempt to share what I learned making a 2.5D game, using the canvas 2D context and plain JavaScript. I would say that 60 FPS is not a reachable goal. Even when the JavaScript engine isn’t stressed.

I recommend you to design your simple sprite game for effective 30 FPS.

You will not regret!

Note: it seems that are 3D games working smoothly on browser at 60 FPS. These kind of games (3D, OpenGl based), are out of scope of the current article.

*      *      *

Disclaimer

This article was also published at JavaScript in Plain English.