Three Quick Wins for the Overlay Stack
Sometimes you don’t need a huge refactor to claw back frame time—you just need to stop redoing the same work every tick. This week we landed three “why were we doing this per frame?” fixes across the overlay/render stack: grid lines now come from an offscreen cache, contextual brush previews blit a pre-rendered mini-level, and the wood asset renderer keeps its tree metadata warm. Together they shave thousands of draw calls and allocations from the render loop, especially when the editor UI is busy.
1. Grid Overlay (EffectsRenderer)
Before: Every frame we nested for y / for x over the visible tile bounds, stroking strokeRect once per cell. On a 140×80 viewport that’s ~11,200 draw calls per RAF tick just for the grid.
After: We paint each unique (cols × rows, color) combination onto an offscreen canvas once and cache it. Per frame we simply drawImage the cached bitmap at the camera offset. Complexity drops from O(cols × rows) per frame to O(1) with a cache miss only when the viewport dimensions or grid color change.
Impact: Grid rendering all but disappeared from the flame graph; enabling the overlay no longer costs 20–25% of the pre-building effects stage on large monitors.
2. Contextual Brush Preview (EffectsRenderer)
Before: The contextual brush preview loop rerendered the entire mini-level each frame—layer by layer, tile by tile—then optionally drew another grid pass on top. That’s essentially a mini TerrainRenderer running inside the effects phase every time you hovered a brush.
After: We pre-render the whole brush LevelV2 into a canvas (WeakMap keyed by the level instance). Hovering now just blits the cached bitmap and, if needed, overlays a cached grid sprite. Brush updates trigger a single re-render; steady-state frames are just drawImage.
Impact: Brush previews now cost the same whether the mini-level has 4 tiles or 400; the frame budget is spent drawing once when the brush changes instead of every mouse move.
3. Wood Asset Renderer
Before: Each render call rebuilt Sets of collected tree keys, filtered preview trees in-line, and recomputed the bobbing phase offset via computePhaseOffsetMs(x, y) for every tree. That meant dozens of allocations and hash math per frame even when the tree list was static.
After: We cache enriched tree data ({ key, phaseOffset }) per tree array in a WeakMap, reuse the same key Set for preview exclusion, and default the animation-key parameter to a shared empty set. The render loop is now “visibility cull → draw” with zero per-frame Set construction or hash recomputation.
Impact: Wood asset rendering is CPU-bound only when the tree list changes (e.g., collecting/dropping wood). During idle frames or camera pans it adds negligible overhead, avoiding GC churn from repeated Set/array creation.
Takeaways
- Offscreen canvases are ideal for static overlays; once you’ve rasterized the pattern,
drawImagebeats any loop ofstrokeRect. - WeakMap caches pair nicely with editor data—when the source array gets replaced you rebuild, otherwise every render reuse pays off.
- When you find yourself recomputing hashes or filtering arrays every frame for data that rarely changes, you’re looking at pure churn. Cache the derived data and let the frame loop focus on drawing.