Buildable Highlight Strings, Be Gone
Our level editor paints several highlight layers—white buildable tiles, green previews, red non-buildable Xs, and purple barracks effects. Each layer used a Set<string> of "x,y" keys, and every render frame we split and parseInted each entry before drawing, even when most of the tiles were nowhere near the camera. The profiler showed String.split sitting inside the render loop’s hot path, so we swapped the whole system for numeric caches and viewport culling.
The Old Path
for (const key of highlightSet) {
const [xStr, yStr] = key.split(",");
const x = parseInt(xStr, 10);
const y = parseInt(yStr, 10);
drawHighlight(x, y);
}
- Four separate highlight sets meant the above loop ran multiple times per frame.
- Large highlight sets (1,000+ tiles) resulted in thousands of
split/parseIntcalls every tick. - Every tile did canvas math even if it was far outside the viewport; no early exits.
- Net cost grew linearly with the total highlighted area, not the visible portion.
The New Strategy
- Coordinate caching — Each
Set<string>now feeds a WeakMap cache that stores{ key, x, y }structs. The string is parsed exactly once per key; subsequent frames reuse the numeric data unless the set’s membership changes. - Viewport bounds — Before drawing we derive visible tile ranges from the camera offset and canvas dimensions. Cached coords outside that range are skipped without touching draw code.
const coords = getHighlightCoords(highlightSet); // cached structs
for (const coord of coords) {
if (!isCoordWithinBounds(coord, visibleBounds)) continue;
drawHighlight(coord.x, coord.y);
}
Results
| Scenario | Before | After |
|---|---|---|
| 1,500 highlight tiles | ~6,000 string parses per frame | Strings parsed once, then reused |
| 60% off-screen highlights | Still computed canvas coords for all 1,500 tiles | Skip ~900 tiles before any math |
| Complexity | O(n) string parsing + full canvas math | O(k) cache rebuilds (k ≤ n) + O(v) on-screen work |
Highlights now contribute virtually nothing to frame time when the camera is stationary, and even during placements we only pay for tiles that actually intersect the viewport.
Takeaways
- If you’re storing coordinates as strings for convenience, consider caching their numeric form as soon as the data stabilizes.
- Pair spatial caches with viewport bounds so your render pass scales with what’s visible, not with the total dataset size.
- Tooling-quality-of-life improvements (clearer highlight overlays) shouldn’t come at the cost of string-parsing hotspots; caching keeps both UX and performance happy.