Optimizations for HTML + JS Mobile Game Development
Performance is one of the main challenges of building an app in HTML + JS. Below are some optimization techniques that I've used in Pocket City, a hybrid Cordova app, to deliver the smoothest experience possible.
Performance is one of the main challenges of building an app in HTML + JS.
Below are some optimization techniques that I've used in Pocket City, a hybrid Cordova app, to deliver the smoothest experience possible.
Contents:
JavaScript
1. Object pools
Instead of creating and destroying many objects, create a pool of them to re-use the same instances. With a pool, you can pre-allocate objects into the pool to be used on-demand. Objects should be returned to the pool when they are "destroyed". You only need to create new objects if you run out of free objects in your pool.
Futher reading on object pools: https://www.html5rocks.com/en/tutorials/speed/static-mem-pools/
In pocket city, most entities come from a pool. For cars, I start with a pool of about 15, and randomize the color when I take one from the pool and add it to the world. When a car is "destroyed", it gets freed back into the pool.
Examples of pooled entities in Pocket City: vehicles, pedestrians, isometric tiles.
2. Re-use arrays and objects
It's easy to accidentally create many new objects and arrays in your game loop. Try to re-use the same object by or array by clearing it first, instead of creating a new one each time.
- An object can be cleared by deleting all keys.
- An array can be cleared by setting it's length to 0.
In Pocket City, I often have 2d arrays of values, like so:
[
[0.0,0.0,0.0,1.0],
[0.0,1.0,0.0,1.0],
[0.5,0.0,0.5,1.0],
[0.2,0.0,0.0,1.0],
[0.1,0.0,0.0,1.0],
]
These might represent things like crime density, traffic, etc. When updating these arrays, I update the values of the existing array instead of allocating a new 2d array each time.
Another example: Returning coordinates like {x:1, ,y:1}
, can end up creating many objects when executed in the game loop. I sometimes re-use the same {x:int, y:int}
object as the return object for many functions, (as long as the code is aware that it could be mutated by another object and uses the x and y values right away).
I usually have a helper function to do this. So instead of
return {x:1, y:2};
I have:
return Utils.getResusableCoord(1, 2);
where Utils.toResusableCoords()
updates an existing re-usable object with the x y coords. Reusable
in the name helps indicate that the value might be re-used by other functions.
3. Break up long tasks into smaller tasks spread over many frames.
There may be functions that take a long time to execute in a single frame. In Pocket City, this might include calculations like power coverage, traffic density, desired # of entities to spawn etc. These functions need to calculate values for the entire 52x52 grid. If it had to do this in a single frame, it would make the game choppy.
Instead of calculating the entire 52x52 grid, the function could be broken up to calculate a 6x6 block on each frame instead of the full 52x52. The execution would them be spread through several frames instead of a single one. Indeed, the functions end up executing longer than a single frame, but this is preferable as it allows the rest of the game to continue, instead of blocking everything.
Implementation-wise, I might queue up several callbacks, each one calculating a chunk of the total calculation, and pop + execute each callback on each frame.
Calculating power in a single frame may cause long frames, so it's spread across several frames
4. Defer & combine execution of frequently called functions
There may be functions that are triggered by several events in an event-based system. For example, two things in Pocket City might trigger an autosave:
- a new building was created
- a quest was completed
If both of these happen at the same time, the autosave might be triggered twice, but it would be preferable to call the autosave once.
In these cases, I may use a short delay and schedule a function to be called after the delay, but only once, event if it was called multiple times.
e.g. so instead of (accidentally) executing:
city.autoSave();
...
city.autoSave();
it could do:
callOnce(1000, "autosave", () => city.autosave())
callOnce(1000,"autosave", () => city.autosave())
where callOnce
has the sigature callOnce(delay, name, cb)
and schedules the callback to be executed after a delay. callOnce()
would be coded to ignore the second autosave because an existing autosave has already been scheduled (checked by keeping track of the name
parameter that was passed).
An even more fine-grained situation is when two things trigger the same function in a single frame. This could happen in your main game update()
loop. For example, in Pocket City, two things might trigger a function to re-calculate the visibility f all sprites:
- camera was moved
- a new entity was spawned
If both these happen in a single frame, may call an expensive recalculateVisibleSprites()
function twice. This is undesireable, so I would want to defer the execution until the entire update() is done. This could go into a general postUpdate()
function.
E.g. so instead of (accidentally) executing:
recalculateVisibleSprites()
...
recalculateVisibleSprites()
it could do:
callOncePostUpdate("calculateVisibility", () => recalculateVisibleSprites());
...
callOncePostUpdate("calculateVisibility", () => recalculateVisibleSprites());
where callOncePostUpdate()
has the interface callOncePostUpdate(name, cb)
. It would track each cb and call them only in the postUpdate()
once.
5. Remove unused code
The larger your code base is, the larger your file size and the longer it takes the browser to parse and compile (https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/javascript-startup-optimization/#parsecompile). Remove unused code when possible.
If you have properties on your objects that you don't use, remove them up to reduce memory.
In Pocket City, I've actually chopped up the Phaser JS source a lot to remove code that I don't need. This admittedly makes it harder to upgrade Phaser, but I've only had to do it once (basically by re-applying all my changes to the new Phaser version).
Phaser: http://phaser.io/
7. Use web workers for CPU-intensive work
Web workers are great for running tasks that eat up CPU time in a separate thread.
In Pocket City, I use web workers serializing and compressing the current city save state into a string. A large JSON is passed to a web worker, which then runs a compression algorithm to reduce it to a much smaller JSON, which is sent back to the main thread in a callback.
Another use case is pathfinding - I pass a 2d grid to a web worker with a start point and end point, and tell it to run BFS/DFS to find a suitable path.
Note that this makes it a bit harder to code as more of your code has to be async, since web worker responses are retrieved in callbacks.
Remember that web workers don't have access to the same state as your main JS thread, so it's most useful when you can simply give it an input and receive an output, without worrying about state.
The save state is serialized and stringified in a separate thread, spawned by the Web API Worker
Web workers: https://developer.mozilla.org/en-US/docs/Web/API/Worker
8. Avoid global variables (and other memory leaks)
Keep memory usage as low as possible. One thing that causes memory leaks is introducing global variables. In Pocket City, there was a memory leak that happened when the user changed or created a new city. The old city object was still reference, with all of its entities still attached, not garbage collected. The reason is because one entity was actually set to a global variable for debugging purposes, and I forgot about it. It had a reference to the city object, so the document root still has a reference path to the city, preventing it from being GC'd.
Using the profiler helped me determine the issue:
https://developers.google.com/web/tools/chrome-devtools/memory-problems/heap-snapshots
12. Cache as much as possible
Cache as many calculations as possible. If a value is unlikely to change frequently, or if it's not essential to recalculate the value on each frame, cache the existing value.
For DOM references (jQuery or plain JS), they can be cached as well.
The main build menu has a cached jQuery selector to quickly show and hide it
Do whatever you can to avoid re-computing the same value twice, and don't recalculate it until you have to. Sometimes this is done using a dirty flag.
http://gameprogrammingpatterns.com/dirty-flag.html
13. Use efficient algorithms/data structures
There are many patterns for game programming - my favorite read on the topic is Game Programming Patterns (http://gameprogrammingpatterns.com/). One important pattern is spatial partitionining. You don't want to loop over each entity every time you need to calculate collisions, so you group nearby entities together.
http://gameprogrammingpatterns.com/spatial-partition.html
In Pocket City, this is used often since the game is divided into a grid system anyway, and each entity is tied to a tile on the grid. Collision checks against other entities are done on nearby tiles only, without looping through all entities in the world.
14. V8 micro-optimizations
Many micro optimizations exist for making your code efficient when running in the V8 engine. I try not to focus on these too much, but there are some interesting tips.
Some good links on V8 optimizations:
https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e
https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments
HTML and CSS
1. Detach DOM nodes
As the DOM of your app grows, the longer it will take the browser to traverse, slower down performance. One way to help is to temporarily detach DOM nodes that aren't used. In Pocket City, the DOM nodes for modals are detached when they are hidden, and re-attached when they need to be shown. This allows the repaints to be faster when the modals are detached.
I use jQuery for this, with .detach()
and .append()
Modals like the Settings modal are detached elements until they are shown, at which point they are re-attached and animated in
2. Use CSS transform
instead of offsets like top
or margin
for animations
In Pocket City, the animation for any moving DOM element is done using CSS animation
or transform
properties, never with properties like top
. This is a well-known optimization for web animations, and this article explains it in detail: https://www.html5rocks.com/en/tutorials/speed/high-performance-animations/
4. Use layer hints for DOM elements that change
Use the CSS property will-change
for elements that are often updated:
https://developer.mozilla.org/en-US/docs/Web/CSS/will-change
Use it sparringly. In Pocket City, it is used primarily for the money and population numbers.
Game Loop
1. Cull out of bounds entities
One of the first optimizations you hear about is culling entities, basically not rendering things that are't in view.
When your game has many entities, you only want to call your update()
on entities in view. You can do check first to determine if the entity is even in view of the camera, before proceeding executing its more expensive update operations.
Buildings that are out of the camera aren't rendered or updated
2. Avoid looping over all entities
In the game engine I use, PhaserJS, the default behaviour of updating the children of the world is to loop through all of them. In my code, I've updated the behaviour so that I track two arrays - children
and visibleChildren
.
children
holds all children, and visibleChildren
only the visible children. I only loop over and update visible children on each frame. Visible children are recalculated every few seconds, or from triggers such as camera movement. Technically I could just loop over the array of all children and simply not call their update()
function if they are out of bounds - but I want to avoid looping over them altogether and avoid the visibility check.
3. Reduce animations on zoom out
If your game has a camera that can zoom out, or if there are entities that move further away from the camera, you could consider ignoring animations and skipping updates when entities aren't in focus and aren't very visible to the player.
For example, when the camera is zoomed out in Pocket City, animations for pedestrians walking and cycling are actually paused temporarily, because the player can't see them clearly anyway and they just look like moving dots, so why bother animating them?
4. Profile, profile, profile
Find the slowest part of your game loop and optimize it. Chrome has great profiling tools for this. Determine what takes the most time in your game loop, and aim to reduce it or break it up over many frames. You can also determine the causes for stuttering and random long frames.
In Chrome, you can even simulate a slower device, which helps to mimic how your app might behaveon weaker devices.
Read all about it in the Google docs: https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/
Thanks for reading
That's all for now! Thanks for reading and for keeping up with the development of Pocket City. I'll continue to optimize the game to be smoother, and look for more techniques to make things faster.