Measuring the JSON.stringify Performance Trap: Phase 4 Task 1 Baseline
Before writing a single line of code for Phase 4 Task 1, we measured—and the numbers confirmed our suspicions.
Baseline results: Hovering over the level editor test level triggers 663 renders in 10 seconds (~66/sec) and allocates 20 MB of temporary strings. React’s memoization is completely bypassed because JSON.stringify()
invalidates dependencies on every mouse move.
This article documents the baseline measurements. Read the implementation and results →
Part of the Phase 4 Frontend & Backend Optimization series.
Task 1: The Problem
When hovering over the level editor with a building selected, ghost highlights show where you can place it. The rendering logic uses React’s useMemo
and useEffect
hooks to avoid unnecessary work, but the dependency arrays look like this:
// client/src/components/GameplayLevelCanvas.tsx:308-312
// client/src/components/TestLevelCanvas.tsx:290-294
useMemo(() => {
// expensive ghost highlight calculation
}, [JSON.stringify(ghostCellPositions || [])])
The trap: JSON.stringify()
creates a new string on every mouse move. Even if the array contents are identical, the string is a new object, so React thinks the dependency changed. Memoization fails, and expensive calculations re-run on every frame.
The same pattern appears in useLivePreviewBuildableHighlights.ts:32-42
, forcing the effect to re-run for every pointer move.
Baseline Measurement Setup
Setup:
- Building Selected: House (first building in Test Level section)
- Test: Hover mouse over canvas in circular pattern for exactly 10 seconds
- Tools: React DevTools Profiler + Chrome Performance panel + Memory Profiler
Files being profiled:
client/src/components/GameplayLevelCanvas.tsx:308-312
client/src/components/TestLevelCanvas.tsx:290-294
client/src/hooks/useLivePreviewBuildableHighlights.ts:32-42
Baseline Measurements
Metric | Value | Impact |
---|---|---|
Peak memory | 68.4 MB | All from temporary allocations |
String allocations | 19.8 MB (41% of total) | Created and discarded on every mouse move |
GC cycles | 6-8 during test | Browser constantly pausing to clean up |
The issue: Every JSON.stringify(ghostCellPositions || [])
call creates a new string object. Even when array contents are identical, React sees a different string reference and invalidates memoization
Expected Improvement
Replace JSON.stringify()
with stable array references via a useStableArray
hook:
Metric | Current | Target | Improvement |
---|---|---|---|
String allocations | 19.8 MB | <10 MB | Eliminate waste |
GC cycles | 6-8 | 1-2 | Smoother experience |
Memoization | Broken | Working | Skip unchanged renders |
Why Measure First?
This “measure → fix → measure” approach delivered Phase 3’s 432x memory reduction with zero regressions. It ensures:
- Baseline data proves the problem exists (not just assumptions)
- Metrics guide implementation decisions
- After-measurements validate improvements objectively
- Rollback decisions are data-driven
What’s Next
The useStableArray
hook will cache arrays in a ref and only return new references when contents actually change—letting React’s memoization work as intended.