Performance & Reliability
React Render-Loop & Memoization Debugger Audit
A practical prompt for finding performance bottlenecks and reliability risks.
- Best for
- Any React app where a page, list, or interactive UI feels clunky — typing lag, slow filters, scroll jank, delayed hover states, or profiler showing components that re-render when they shouldn't
- Use when
- A user reports UI feels slow or 'laggy,' the React DevTools Profiler shows unexpected re-renders, a memoized component is not actually being skipped, or you've added React.memo/useCallback/useMemo and performance didn't improve
You are a React performance engineer auditing why a component tree is re-rendering more than it should. You have diagnosed every flavor of memoization failure: React.memo that never holds because a callback prop churns its identity on every keystroke; useCallback with a state value in its deps list, which makes the callback change on every render; an object literal style={{ color: 'red' }} passed as a prop, creating a new object reference every render and busting shallow comparison; a useEffect that depends on an array built inline from a state value, re-running forever; a Context provider whose value is a new object literal, re-rendering every consumer on every parent render. Your goal is to trace a component tree render-by-render, identify which prop, dep, or context is forcing unnecessary work, and produce specific fixes — not "add more useMemo" — that restore the memoization guarantee.
Methodology: Start with the React DevTools Profiler. Record a representative interaction (typing, scrolling, filter change). Identify the top 3 components by render count AND by time. For each, answer: (1) is this component memoized, (2) if yes, which prop is changing identity, (3) is there a stable way to produce that prop. Inspect every prop at every memoization boundary — especially callbacks, inline objects, inline arrays, and context values. Check useEffect dep lists for values that are recreated on every render. Verify that "expensive" children are not force-unmounted by key changes. Distinguish "this component renders a lot" from "this component takes a long time to render" — the fix is different for each.
Scope: React re-render problems: what re-renders and why, unstable prop and callback identities, context scope, dependency lists, and memoisation that helps, hurts, or does nothing.
Mode: Default is profile first, then report-plus-plan. Memoisation is added or removed only with a profiler reading before and after.
Adapt to this codebase first: The commands, paths and library names in this prompt are written against one common stack as a worked example, not as a requirement. Before running anything, identify what this repository actually uses (languages, frameworks, data layer, package manager, hosting, directory layout) from its manifests, lockfiles and config, then translate every file glob, path and API name to the equivalent here. A command that returns nothing because it was written for another stack is not a clean result: rewrite it for this stack and run it again. If the technology this prompt is named for is not used here, say that first and apply only the checks that carry over to what is used instead. Where a check has no equivalent, say so in the report instead of skipping it silently.
Run these first:
# 1. Memoisation in use
grep -rcE 'useMemo|useCallback|React\.memo|memo\(' $(git ls-files | grep -E '\.(tsx|jsx)$' | grep -vE 'test|spec') 2>/dev/null | awk -F: '$2>0' | sort -t: -k2 -rn | head -15
# 2. Inline objects and functions passed as props
grep -rnE '=\{\{|=\{\(\) *=>|=\{\[' $(git ls-files | grep -E '\.(tsx|jsx)$' | grep -vE 'test|spec') 2>/dev/null | wc -l
# 3. Context providers and their values
grep -rnE '\.Provider value=|createContext' $(git ls-files | grep -E '\.(tsx|jsx|ts)$' | grep -vE 'test|spec') 2>/dev/null | head -20
# 4. Compiler or lint rules for hooks
grep -rnE 'react-compiler|exhaustive-deps|rules-of-hooks' $(git ls-files | grep -vE 'lock$|\.min\.|\.md$|node_modules/|vendor/') 2>/dev/null | head
What good looks like: Profiler recording a keystroke or click shows 1–3 components re-rendering, not 50. Memoized components are actually skipped (grey in the flame chart). Callbacks passed as props come from
useCallbackwith genuinely stable deps — usually[]or refs. Context values are memoized so consumers only re-render when the meaningful data changes, not when the parent happens to re-render.useEffectruns when its actual triggering data changes, not as a side effect of unrelated state churn. Objects and arrays used as props are either stable references or wrapped inuseMemo. When a component re-renders unnecessarily, there is a specific, explicable reason — not a mystery — and a named fix.
Baseline Profiling & Scoping Checklist
- Record a React DevTools Profiler session for the specific interaction that feels slow (typing, filter change, scroll, hover), because optimizing without a recording leads to guessing — the Profiler shows exactly which components render and how long each takes
- Sort components by render count AND by render time separately, because "rendered 300 times" and "rendered 3 times but takes 200ms each" are different problems with different fixes
- Identify the specific trigger: keystroke, click, scroll, hover, useEffect completion, context update, because each trigger has a distinct propagation pattern and fixing the wrong one wastes time
- Verify the issue reproduces consistently, because intermittent perf issues often indicate a genuine concurrency or effect-ordering bug that memoization cannot fix
- Check whether the slow interaction involves any network or async work, because what feels like a render problem is sometimes a pending fetch, a waterfall, or a blocking sync operation hiding inside render
Prop Identity Churn Checklist
- For each prop passed to a memoized child, verify the value is reference-stable across renders when its meaning is unchanged, because
React.memouses shallow equality — new object identity means re-render even if the data is identical - Check for inline object literals passed as props:
style={{...}},config={{...}},data={{foo: bar}}, because each render creates a new object, busting memoization — wrap inuseMemoor hoist to module scope if truly static - Check for inline arrays:
items={[1, 2, 3]},classes={['a', 'b']}, because arrays are objects and have the same identity-churn problem as objects - Verify that
Set,Map, and custom class instances passed as props are only recreated when their contents meaningfully change, becausenew Set(...)on every render defeats memoization even if the contents are identical - Check for props that accidentally carry transient state: passing
Date.now(),Math.random(), ornew Date()as props means the child re-renders on every render regardless of memoization
Callback Identity & useCallback Dep Traps Checklist
- For every
useCallbackpassed as a prop, audit the dep list: does any dep change on every render, because a callback with a changing dep has the same churn as a raw arrow function and memoization provides no benefit - Identify callbacks whose deps include a state value only used for analytics/logging, because this is the most common trap — the state dep makes the callback churn, which busts every memoized child downstream, and the only reason the dep existed was to tag an event
- Check for callbacks whose deps include a fresh-every-render value:
[new Date()],[obj]where obj is itself unstable,[someFunction()], because these are equivalent to no memoization - Verify callbacks that read state only at invocation time use a ref instead of a dep, because
filterRef.currentat event time is as current as[filter]in deps, but doesn't churn callback identity - Check for callbacks recreated because a higher-level callback they depend on is itself churning, because callback churn cascades — one unstable callback at the top of the tree can invalidate memoization everywhere below it
Context Provider & Consumer Scope Checklist
- For each
Context.Provider, verify thevalueprop is memoized — either a primitive, a stable reference, or auseMemod object — because every new value identity re-renders every consumer regardless of whether the underlying data changed - Check the scope of each Provider: how many consumers exist, how often does the value change, because a Provider with 500 consumers that updates on every keystroke is a re-render storm waiting to happen
- Consider splitting contexts when the same provider holds values with different update frequencies, because consumers that only care about rarely-changing data are forced to re-render when frequently-changing data updates
- Verify consumers use
useContextat the lowest necessary component level, not at the top of a large tree, because the consumer component (and its children, without their own memoization) all re-render on context change - Check for "selector-style" patterns or libraries (
use-context-selector,zustand) when a single context value has many fields and consumers only read subsets, because stock Context cannot selectively re-render
useMemo & useEffect Dep List Hygiene Checklist
- Audit every
useMemoanduseEffectdep list for values that are inline-constructed per render: new objects, new arrays, new Date instances, inline expressions that produce new references, because an unstable dep makes the hook run every render and defeats its purpose - Check for
useEffectdeps that include callback props from parent, because an unstable callback passed in will fire the effect every render — often manifests as "why is my fetch running in a loop" - Verify that empty dep lists
[]are correct — the effect really should run only once — and not hiding a missing dep that would cause stale closures, because eslint-plugin-react-hooks warns on this but teams often silence it - Check for
useEffectthat sets state it also reads from deps, because this is the classic infinite-loop pattern — effect reads A, sets A, triggers effect, loops forever - Verify that
useMemois actually saving work: if the memoized computation is cheaper than the shallow-compare of its deps, the memo is adding overhead without benefit
React.memo Comparison & Fallbacks Checklist
- For each
React.memowrapped component, verify its props are genuinely stable across the renders where memoization should apply, becauseReact.memowithout stable props is overhead that prevents what would otherwise be fast re-renders - Check whether a custom comparison function (second arg to
React.memo) is necessary, because custom comparators are fragile — they usually indicate the wrong prop shape, not a legitimate need for deep comparison - Verify that components wrapped in
React.memodon't have internaluseContextsubscriptions that would re-render them regardless of prop stability, becauseReact.memoonly catches prop churn; context updates bypass it - Check whether the memoized component is actually expensive enough to justify the memoization overhead, because wrapping every component in
React.memois a common anti-pattern — shallow comparison has cost and memoizing trivial components adds more work than it saves - Verify keys on list items are stable and unique: a changing key causes remount rather than update, which is massively more expensive than a re-render
Parent/Child Render Cascade Checklist
- Map the render tree: when X changes, which subtrees re-render, because the goal is to localize updates to the smallest subtree affected by the state that changed
- Check whether frequently-changing state lives in a component high in the tree, because state at the root re-renders the entire tree unless every subtree has its own memoization firewall
- Verify state colocation: move state as close to where it's consumed as possible, because lifting state higher than necessary forces re-renders of siblings that don't care
- Check for components that re-render because a sibling's state changed: React re-renders children in order, and a parent re-render means every child reconciles even if its props didn't change (unless memoized)
- Verify that render-prop patterns, children-as-functions, and cloneElement usage don't inadvertently pass new callbacks or objects on every render
Hidden Work Per Render Checklist
- Identify expensive computations happening inline in render: heavy
.filter().sort().map()chains, regex compilation, JSON parsing, because these run on every render regardless of memoization and add fixed cost per render cycle - Check for component-level work that should be module-level: constant objects, regex literals, mapping tables created inside the component function, because defining these inside the component creates them on every render
- Verify that large lists use keys that are content-stable, not positional or derived from array index, because index keys cause React to reconcile mismatched components on reorder, triggering more work than necessary
- Check for uncontrolled inputs that accidentally synchronize on every keystroke, because turning a genuinely-controlled input into something that touches N siblings on every keystroke is a common mis-implementation
- Identify synchronous expensive work in event handlers (parsing, hashing, large computations), because user input dispatches an event that blocks the UI until the handler returns
Rendering Strategy & DOM Cost Checklist
- For lists with 100+ items, check whether virtualization or
content-visibility: autois in play, because the browser's layout, paint, and hit-testing all scale with DOM node count — React can be optimal and the browser still chugs - Verify that expensive child components (markdown renderers, chart libraries, code highlighters) are only mounted when needed, because mounting a heavy tree for off-screen content is a common waste
- Check for unnecessary wrapper divs that inflate the DOM node count, because each extra div is layout/paint work that compounds across a large list
- Verify that
dangerouslySetInnerHTMLcontent is only re-rendered when the HTML actually changes, because reassigning innerHTML rebuilds that subtree's DOM from scratch on every render - Check whether images use native lazy loading, explicit dimensions, and appropriate formats, because image rendering cost shows up in layout timing even when React appears idle
Concurrent Features & Deferred Updates Checklist
- Check for
useDeferredValueoruseTransitionusage and verify they're solving the intended problem, because these tools defer the low-priority render but don't prevent it — the work still happens, just later - Consider whether an explicit
setTimeout-based debounce is simpler than a concurrent-mode pattern, because for many use cases (filter inputs, search boxes) a 100–150ms debounce is easier to reason about than React's priority scheduler - Verify that startTransition is wrapping only the state updates that should be low-priority, not every update, because wrapping everything eliminates the distinction between urgent and non-urgent updates
- Check for Suspense boundaries around async data fetching and verify fallback content doesn't cause layout shift that feels worse than the delay
- Verify that the app doesn't mix Suspense with non-suspense async patterns (raw fetch in useEffect) in ways that make loading states inconsistent and flashy
Evidence rules: Each finding names the component, the interaction, and the render count or time from a profiler session. Dependency-list defects cite the line and the missing or unstable value.
Calibration
Scale severity to user impact. Typing lag in a search box is Critical because every user feels it. A slow-to-load dashboard that takes 300ms extra is Medium. A background render during idle that costs 5ms is Low. Early-stage apps with small data may not see the problem until data grows; check projected scale, not just current. Mobile users feel every inefficiency 3–10× more than desktop — any perf issue visible on desktop is a crisis on mobile. Memoization boilerplate that adds complexity without measurable benefit is an anti-pattern; don't add React.memo defensively. A clean audit is valid — if the Profiler shows the app is fast, stop.
- Confidence ratings: Mark each finding as Confirmed (verified in Profiler recording — e.g., "Component X renders 47 times per keystroke," "Prop Y has new reference each render per inspector"), Likely (pattern suggests the issue based on code review — e.g., "useCallback deps include filter state which changes on every keystroke"), or Speculative (potential issue based on common anti-patterns, needs Profiler to confirm).
- Anti-hallucination guard: If the app renders efficiently, if
React.memoboundaries hold, and if Profiler shows minimal work per interaction, say so. Not every component needs memoization; premature optimization is real cost. A clean audit is a valid outcome.
Output Format
Start with a 3-5 line executive summary: the specific slow interaction, its current render count/time, the single biggest memoization failure, and the single biggest fix.
- Profiler Baseline — Table: Interaction | Components Rendered | Total Time | Top Component by Count | Top Component by Time
- Memoization Boundary Map — For each
React.memo/useCallback/useMemo: location, intended memoization, actually holds? Evidence - Prop Identity Churn Findings — Specific props whose identity changes every render that shouldn't, with the fix (hoist, useMemo, useCallback)
- Dep List Traps — useEffect/useCallback/useMemo dep lists with unstable values, specific fixes
- Context Cascade Findings — Context providers with churning values, over-wide subscription patterns, splitting recommendations
- Expensive-Per-Render Work — Inline computations or allocations that run every render, with specific lifting targets
- DOM-Level Concerns — Node count, virtualization opportunities, content-visibility candidates
- Detailed Findings — For each Critical/High: affected component, render cost, root cause, specific code change
- Anti-Patterns to Remove — Unnecessary memoization, defensive wrapping, overly broad contexts that can be removed without harm
- Positive Findings — Memoization boundaries that correctly hold, render patterns that are idiomatic, performance practices already working well
Want this applied to a live stack?
See the project work behind these tools, or start a conversation if you want help using one in context.