36% Memory Reduction: Phase 4 Task 1 Results
We replaced JSON.stringify() with stable array references in the level editor’s ghost highlight system. The result: 36% reduction in peak memory usage (24.7 MB saved) with smoother and more stable garbage collection patterns.
Part of the Phase 4 Frontend & Backend Optimization series. Read the Task 1 Baseline Analysis first.
The Fix: useStableArray Hook
The problem was simple: JSON.stringify() creates a new string object on every mouse move, breaking React’s memoization. The solution was equally simple: cache array references and only update when contents actually change.
// client/src/hooks/useStableArray.ts
function useStableArray<T>(arr: T[] | null | undefined): T[] {
  const ref = useRef<T[]>(arr || []);
  // Only update ref if array contents changed
  if (!arraysEqual(ref.current, arr || [])) {
    ref.current = arr || [];
  }
  return ref.current;
}
Applied to 3 locations:
- GameplayLevelCanvas.tsx:308-312
 - TestLevelCanvas.tsx:290-294
 - useLivePreviewBuildableHighlights.ts:32-42
 
Before:
useMemo(() => {
  // expensive calculation
}, [JSON.stringify(ghostCellPositions || [])])
After:
const stableGhostPositions = useStableArray(ghostCellPositions);
useMemo(() => {
  // expensive calculation
}, [stableGhostPositions])
Results Comparison
| Metric | Before | After | Improvement | 
|---|---|---|---|
| Peak memory | 68.4 MB | 43.7 MB | -24.7 MB (36%) | 
| Memory range | 35.1-68.4 MB | 35.7-43.7 MB | Narrower, more stable | 
| String allocations | 19.8 MB (41%) | 22.4 MB (44%)* | Spread more evenly | 
| GC spikes | Infrequent but disruptive | More frequent but gentler | Less noticeable pauses | 
String allocation percentage is similar, but in the “After” case allocations are steadier and spread over time instead of building into large spikes.
Memory Graph Comparison
Before (Baseline):

- Large sawtooth pattern with infrequent, aggressive GC cycles
 - Orange line (retained memory) rises and does not come back down, suggesting references remain alive unnecessarily
 
After (Optimized):

- Heap stays in a much narrower band
 - GC runs more frequently, but cleans up smaller amounts — less disruptive for interactivity
 - Orange line spikes a couple of times but then stays flat at a low level, showing healthier retention
 
What Changed (And What Didn’t)
✅ Improved
Memory stability: The memory graph shows much smoother allocation patterns without aggressive spikes.
GC pressure: More frequent but gentler garbage collection cycles—less disruptive for interactivity.
Render efficiency: Flamegraph bars are noticeably thinner, confirming memoization now works correctly.
📊 Similar (As Expected)
Commit counts: Still ~550-600 commits during the 10-second hover test. This is expected—mouse move events still trigger renders, but each render is cheaper because memoization works.
String allocations: Percentage remains around 41-44%. The difference is that allocations are more controlled and spread over time rather than spiking aggressively.
🎯 Real Impact
No perceived lag: The level editor feels smooth during hover interactions, especially on lower-end devices.
Better memory headroom: Provides breathing room for other game systems without triggering aggressive GC.
Foundation for future work: Stable array references set the pattern for Tasks 2-4.
Phase 4 Task 1 Summary:
- ✅ 36% peak memory reduction (24.7 MB saved)
 - ✅ Smoother memory patterns (fewer GC spikes)
 - ✅ Memoization working (thinner flamegraph bars)
 - ✅ Zero functional regressions
 - ✅ Foundation for Tasks 2-4
 
Implementation and measurements completed using Claude Code with document-driven development approach.