whyrn

avoidable re-render finder for React Native

why did
this render?

UserCard - same value: user useMemo in ProfileScreen

WhyRN watches every render and reports only the wasted ones that cost frames - with the prop or context to fix and the component to fix it in. Real changes and cheap renders stay quiet. Wrap your app once. Dev builds only.

npm i -D whyrn.dev

§1

Press a button. Only waste flashes.

A small React Native screen, simulated. A real state change stays quiet; renders that changed nothing flash, and Metro says what to fix.

Profile tick 0

Counter count 0
UserCard Alice
MemoBox React.memo · stable props
ThemeLabel theme light
renders / 5 s
  1. 1
  2. 2
  3. 4
  4. 6
  5. 8+
metro ▸ waiting for an avoidable re-render…

§2

What gets flagged, and the fix.

A render is flagged when nothing the component depends on really changed and the waste adds up to a frame (16 ms) per second. The summary names the parent where the unstable value is created.

cause metro says fix
object prop "user": new object with the same content - useMemo in Screen An object, array or JSX literal is rebuilt on every render of the parent. useMemo it, or hoist it out of the component.
function prop "onPress": new function every render - useCallback in Counter An inline handler. useCallback in the parent.
memo props are equal - wrap FlexChild in React.memo Nothing changed for this component; it only followed its parent. React.memo skips it.
context ThemeContext got a new value with the same content - useMemo the Provider value The provider passes a fresh value object, so every consumer renders.
store Store selector returned a new object with the same content A Zustand / Redux selector builds a new object. Select primitives, or use a shallow-equal selector.
quiet state[0] changed: 14 → 15 A real change - or a wasted render too cheap to matter. Not reported by default; report="avoidable" or "all" shows them.

§3

Setup is one wrapper.

No init call, no Babel plugin, no native module. Bare apps, Expo Go, dev clients; old and new architecture.

01 · install

npm i -D whyrn.dev

02 · wrap · App.tsx

import { WhyRN } from 'whyrn.dev'

export default function App() {
  return (
    <WhyRN>
      <YourApp />
    </WhyRN>
  )
}

03 · read · metro

⚠️ WhyRN: 9 avoidable re-renders
  UserCard ×3 · rendered by Screen
    prop "user": new object with the
    same content - useMemo in Screen
  Button ×6 · rendered by Counter
    prop "onPress": new function every
    render - useCallback in Counter

§4

Four exports.

Full reference in the README ↗

<WhyRN>

Tracks everything below it and draws the overlay. React Native internals - View, Text, Pressable, Animated.* - are always skipped.

enabled
__DEV__
kill switch
report
critical
avoidable: all waste · all: every render
criticalMs
16
wasted ms per second to report
heatmap
false
persistent boxes, cold → hot, ×N
include
[]
only names matching these regexes
exclude
[]
skip names matching these regexes
trackHooks
true
report state, context, store
logToConsole
true
print to Metro
flashDuration
600
ms
flashColor
#FF6B6B
when heatmap is off

withWhyRN(Component)

Tracks one component, even without <WhyRN> (console only, then). Returns the same component - no wrapper, no extra view.

export default withWhyRN(UserCard)

useWhyRN(name, props)

The reasons for the current render, as data - for your own debug UI or a test assertion.

const reasons = useWhyRN('UserCard', props)
// [{ type: 'props', propChanges: [...] }]

useRenderCount(name?)

How many times this component has rendered. Pass a name to log each re-render.

const n = useRenderCount('FeedItem')

§5

It watches. It doesn't touch.

WhyRN listens to React commits through the channel React DevTools uses.

  1. listen to each commit on __REACT_DEVTOOLS_GLOBAL_HOOK__
  2. walk only the fibers that did work; diff props, hook state and context against the previous version
  3. classify a render as avoidable when no prop changed by value, state changed only to an equal value, and no context or store pushed new content
  4. measure the native views each component rendered with measureInWindow
  5. draw a fading box and a badge in one overlay on top of the app - tap the badge to open the JSX in your editor
  6. link every problem to the line that renders it - Metro symbolicates React's dev element stack into file:line:col
  • patch createElement or hooks
  • wrap components in extra views
  • change component identity (navigators keep working)
  • run in release builds

§6

Your agent can use it too.

The package ships whyrn-debug, an Agent Skill for Claude Code and Codex: measure, fix the cause the log names, measure again - instead of memoizing everything on a hunch.

01 · link once, commit

mkdir -p .claude/skills .agents/skills
ln -s ../../node_modules/whyrn.dev/skills/whyrn-debug \
  .claude/skills/whyrn-debug
ln -s ../../node_modules/whyrn.dev/skills/whyrn-debug \
  .agents/skills/whyrn-debug

02 · ask

# Claude Code
/whyrn-debug The product list stutters
while scrolling. Find out why and fix it.

# Codex
$whyrn-debug Same task.

03 · it reads the log

779 re-renders across 11 components

  302  Button  (rendered by Screen)
    prop "onPress": new function
    every render - useCallback
   66  ThemeLabel
    ThemeContext got a new value
    with the same content

Plain-text docs for LLMs: whyrn.dev/llms.txt · SKILL.md ↗