我的小型RPG性能问题的原因是什么
What Caused Performance Issues in My Tiny RPG

原始链接: https://jslegenddev.substack.com/p/what-caused-performance-issues-in

## Tiny RPG 性能深度分析 一位开发者在将他们的 JavaScript RPG(使用快速原型库 KAPLAY 构建)打包为桌面应用程序时遇到了严重的性能问题,具体表现为运行速度比在浏览器中慢。最初在 KAPLAY Discord 上进行的故障排除没有发现类似报告,促使他们通过 Substack 和 Hacker News 进行公开测试。 核心问题源于多种因素的结合。首先,使用 GemShell 创建可执行文件导致在 Mac 上使用 Webkit 引擎,其性能低于 Chromium。其次,KAPLAY 中低效的文本和精灵渲染——为简单元素创建游戏对象,而不是在游戏循环中直接绘制——严重影响了 Chrome 的性能。最后,缺少子弹对象池加剧了 Chrome 和 Safari 的垃圾回收问题。 解决方案包括移除 KAPLAY 中一个有问题的 FPS 限制,优化渲染技术,并实现对象池。虽然性能现在已显著提高(并且可以在 [itch.io](https://jslegend.itch.io/small-rpg-performance-playtest) 上获取更新版本),但这次经历强调了多样化工具知识的重要性。该开发者计划花时间学习更高效的游戏框架,以避免未来的障碍并确保项目的持久性。

Hacker News新 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交登录 我的小RPG性能问题的原因 (jslegenddev.substack.com) 6 分,作者 ibobev 1小时前 | 隐藏 | 过去 | 收藏 | 1 条评论 kg 1分钟前 [–] 有趣的文章。作者在相对简单的场景中遇到如此严重性能问题让我感到惊讶,而且听起来性能问题还没有完全解决。过去我可以用JS+canvas运行相当复杂的2D场景,所以我想知道kaplay或其他他们使用的库下面是否存在某种根本的性能问题?回复 考虑申请YC冬季2026批次!申请截止至11月10日 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请YC | 联系 搜索:
相关文章

原文

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

To build executables for Windows, Mac and Linux, I use either NW.js or GemShell.

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.

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.

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.

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.

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 has apparently 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!

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.

Image

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 game object.

// 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.

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.

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!

联系我们 contact @ memedata.com