October 3, 2025 By WBG Development Team
wildermine performance technical optimization

Measuring the JSON.stringify Performance Trap: Phase 4 Task 1 Baseline

Task 1 baseline measurements reveal 663 renders in 10 seconds and 20 MB of string garbage—confirming our hypothesis before the fix

Measuring the JSON.stringify Performance Trap: Phase 4 Task 1 Baseline

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

Chrome Performance showing baseline measurements

MetricValueImpact
Peak memory68.4 MBAll from temporary allocations
String allocations19.8 MB (41% of total)Created and discarded on every mouse move
GC cycles6-8 during testBrowser constantly pausing to clean up

Chrome Memory Profiler showing string allocations

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:

MetricCurrentTargetImprovement
String allocations19.8 MB<10 MBEliminate waste
GC cycles6-81-2Smoother experience
MemoizationBrokenWorkingSkip 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.

Read the implementation and results →

WBG Logo

Written by WBG Development Team

Part of the passionate team at Wrinkled Brain Games, creating innovative gaming experiences.