What Caused Performance Issues in My Tiny RPG
In a previous post, I mentioned having strange performance issues regarding a tiny RPG I was secretly working on.
The crux of the matter was that the game (built using web technologies) would run noticeably less smoothly when wrapped as a desktop app on my machine than when running in Firefox.
I initially shared the project’s executables on the KAPLAY Discord server (KAPLAY being the library I used to make the game in JavaScript) and none reported the performance issues I had.
Seeing this, I decided to make a Substack post inviting my wider audience to try the game out and report its performance. This led to someone sharing the post on Hacker News which resulted in a large influx of views and feedback.
In this post, I would like to explain what went wrong (as best as I understood it myself).
First of all, I have mostly fixed the performance of my game. It now runs much more smoothly and I have updated the executables on itch. I would still greatly appreciate feedback regarding how these run on your machines. (Download the ones tagged with -v2) Here’s the link : https://jslegend.itch.io/small-rpg-performance-playtest
Context : How is My Game Packaged as a Desktop App
To build executables for Windows, Mac and Linux, I use either NW.js or GemShell.
NW.js
Similar to Electron but easier to set up. It packages a Chromium instance for rendering. Results in bloated executables but the same rendering engine is used across platforms.
GemShell
Similar to Tauri in how it uses the operating system’s webview to render the app rather than packaging a Chromium instance. This results in leaner executables but different web engines with varying performance differences are used on different platforms. However, contrary to Tauri, GemShell is a one click tool that generate executables for all three platforms.
I wrote a post about it recently that delved into more detail.
Since I wanted to build executables quickly, I decided to try out GemShell for this project. Executables that I distributed to get feedback were made with this tool.
Context : Why KAPLAY?
KAPLAY is a library based on the concept of game objects and components. A game object is created from a list of components, many of which, are ready made and offer functionality out of the box. This makes the library very easy to understand and productive.
The issue is that it’s by default less performant than other alternatives (Phaser, Excalibur, etc…)
I use it because it’s so fast to prototype game ideas in. However, I used it for this project because it was the tool I knew best that would allow me to immediately start working on the game and focus on game design.
In hindsight, I should have probably started invested in learning other more performant alternatives to a sufficient level so that I could easily pivot away if the needs of my project demanded it.
Problem #1 : The Game Runs Well Initially and Then Slows Down to a Crawl
This was often reported among people for who the game didn’t perform well.
In KAPLAY, there is an option to cap the FPS to a maximum amount. It’s used when first initializing the library. The idea behind this option is to enforce a consistent frame rate resulting in more consistently smooth gameplay.
kaplay({
// .... other options omitted
maxFps: 60
})However, setting this to 60 in my game resulted in the game eventually slowing down to a crawl all while the debug fps count still displayed 60 fps. I conclude that this happened when the machine running the game wasn’t able to sustain 60fps or more at all times. The fix was simple, remove it.
Why did this option behave this way? Probably a bug a in the library. A maintainer is currently working on it.
Problem #2 : The Game Had Poorer Performance on Mac Compared to Windows
Since GemShell was used to make executables, the underlying web engine used to render the game on a Mac is Webkit and not Chromium. While I knew that Webkit was less performant than Chromium, I didn’t think it would be that noticeable.
The apparent solution to this would be to move off of GemShell and use NW.js instead. However, it turns out that MacOS does certain things to not allow Webkit to use its full potential. The dev behind GemShell apparently has a fix for this situation.
Below is what they shared on their Discord, which you can join if you’re interested in the tool here.
This would be great since it would be nice to have the best of both worlds, more consistent performance across platforms while still having lean executables.
Since I’m still not done with the development of the game, I can afford to let the dev cook, as we say!
Problem # 3 : Even on The Web, The Game Performed Better on Firefox compared to Chrome and Safari
This is the strangest performance issue I experienced. While I could conceive of the game performing better on Firefox VS Safari (since it uses Webkit), I was caught off guard by Chrome performing poorly than Firefox. Chrome is supposed to be more performant due to the Chromium Web engine.
The Chrome profiler seemed to indicate that the way I was rendering text in my game probably needed to change.
KAPLAY Game Objects and Text Rendering
In KAPLAY, if you look at the examples provided in its playground, you’ll find that it’s often shown that to render text you first, create a game object using a text component and then pass the text to that component.
// This will render the text "Hello World!" at the center of the screen
const myTextObj = add([text("Hello World!"), pos(center())]);However, it turns out that this is inefficient since game objects have a greater cost in terms of performance. For rendering simple text it’s far better to draw it directly in the draw loop. Here’s a simple example.
onDraw(() => {
drawText({
text: "Hello World",
pos: center()
});
});The same logic applies to drawing static images like backgrounds. Instead of doing :
const myImage = add([sprite("someImage"), pos(center())]);I needed to do :
onDraw(() => {
drawSprite({
sprite: "someImage",
pos: center()
});
// ... drawText
})Additionally, to not disable batching it was important that all drawSprite calls be placed together in the draw loop before rendering text with drawText calls.
Doing this led to better performance on Chrome and Safari. However, there was one last obvious performance improvement I needed to do.
Reusing Projectiles Otherwise Known as Object Pooling
In the battle section of my game the player must avoid getting hit by a horde of projectiles thrown at them. A common optimization technique used in this case was to reuse bullets that leave the screen rather than creating new ones. This technique is known as object pooling.
I had planned on implementing something like this eventually but didn’t do it since I was developing the game using Firefox as my preview, and the performance was great.
There weren’t that many projectiles in the first place so I felt that this would be useful later. However, considering that Chrome and Safari were struggling performance wise probably due to their garbage collector working differently, I resigned myself to implement it now.
As expected the game performed much better on both Chrome and Safari. For Chrome, I now had a constant 60fps on my machine (Macbook Air M3, 16 GB RAM) but for Safari it was more around 45-60fps.
To stress test my pooling system, I added a bunch of projectiles. Here is footage.
Conclusion
I’m happy that I can now resume development and focus more on game design. However, what this adventure taught me is that I should probably invest my time learning other frameworks and game engines.
While I eventually ended up fixing the performance issues in my game, I can’t help but think of scenarios where problems could arise later that are unfixable due to limitations of the tools I’m using.
In that case, I would have to halt development to learn a new tool at a proficient level on top of having to reimplement the game which would take a lot of time and result in my project getting significantly delayed.
If I start learning something else right now, I can at least go faster if I eventually need to reimplement the game.
Finally, if you’re interested in keeping up with the game or like technical posts like this one. I recommend subscribing to not miss out when new posts are published.
In the meantime, here are few of my previous posts that could interest you!








