I'm currently tiling many PNG images on several stacked FastLayers with Konva.js. The PNGs contain opacity, and they do not require dragging or hitboxes. The tiles are replaced often, and this seems to work well for medium-sized grids with dimensions of around 30x30. Once the tiles start growing to around 100x100, or even 60x60, the performance begins to slow when replacing individual tiles.
I've started to work on "chunking" tiles, i.e., adding tiles into smaller FastLayer groups. For example, a single 100x100 FastLayer would be divided into several 10x10 FastLayers. When a single tile changes, the idea is that only that chunk should should re-render, ideally speeding up the rendering time overall.
Is this is a good design to attempt, or should I try a different approach? I've looked over the performance tips in the Konva.js documentation, but I haven't seen anything directly relevant to this case.
So, after some research and tinkering, I've discovered the fastest way to render ~4000 images.
- Don't use React components for Konva.js. I use React to structure my app, but I've skipped using an intermediate library for Konva.js rendering. Using React Components for the canvas will halve your performance.
- Cache common images. I use a simple LRU cache to reuse HTMLImageElement objects.
- Reuse Konva.js nodes (Konva.Image) whenever possible. My implementation is rendering a grid of images. The locations do not change, but the images may. Before, I would destroy() a node, and the add another. The destroy() causes an additional render, which creates jank for your users. Instead, I just use the image() method in combination with id() and name() to find and replace images at grid coordinates.
- My app allows users to paint long strokes across the grid. This works OK in small strokes, when only using the literal mouse events. For long strokes, this does not work for two reasons. First, the OS and browser throttle the mouse events, giving you intermittent mouse events. Second, being in the middle of a render will give the same side effect. Instead, the software now detects long strokes, and "fills in" the missing coordinates that the user intended to draw between the intermittent mouse events.
- Render at intervals. Since my grid can change often, I decided to sample the grid information 24 times a second, rather than allowing each tile change to queue up a batchDraw(). The underlying implementation is using RxJS to poll a Redux store once every 42ms, and only queues a batchDraw() if something has changed.