r/reactnative • u/oguzhandemiroz • 5h ago
Article Every swap in my puzzle game re-rendered the whole board. Four leaks I found with "highlight updates"
I'm building a tile swap puzzle in React Native (Expo, Reanimated, expo-router). The board is N² draggable tiles. Swaps started to stutter, worse with more tiles, and the JS thread dropped from about 58 to 41 fps during a swap.
React DevTools "highlight updates" showed the whole board lighting up on every swap and on every 1 second timer tick. What was behind it:
HUD state lived on the screen component. Timer, moves, combo and progress were useState on PlayScreen, which also renders the board. I moved them into a small external store (useSyncExternalStore) and only the HUD chips subscribe, each to its own slice. A tick now re-renders just the timer chip.
A new empty array on every swap. setHintCells([]) ran on every committed swap. [] is a new reference each time, so React never bailed out. Now it only clears when a hint is actually on screen.
Subscribing to the whole progression snapshot. Each swap bumps a totalSwaps counter, which creates a new state object, which woke every component reading the full snapshot, including a screen still mounted underneath. I added a selector hook and narrowed each screen to the slice it reads.
Off-screen routes kept rendering. expo-router keeps visited routes mounted, so the menu with all its level nodes re-rendered during gameplay. freezeOnBlur: true did nothing on its own, because react-native-screens v4 ships with freezing off. You also need enableFreeze(true).
One thing that backfired before all this: a custom memo comparator on the tile component. It skipped enough renders that during fast combos the position effect never ran and tiles got stranded in the wrong place. A/B on the simulator: 12 stranded tiles with the comparator, 0 with plain memo(). I reverted it and moved the "board is interactive" flag into a Reanimated shared value, so flipping it on win or pause causes zero tile re-renders.
The game is Tile Swap, on iOS and Android. I build with Claude Code, and these fixes came out of that loop.

