What this usually means
A slow React app rarely stems from a single expensive render. More often, the cause is unnecessary re-rendering, inefficient diffing, or components that trigger expensive side effects on every update. These bottlenecks may not be obvious from code review or basic logging. Only a structured profiler trace can show you which components are rendering, why, and for how long—surfacing hot paths that would otherwise be missed.
The first ten minutes — establish facts before touching code.
- 1Open React DevTools, profile a slow user action, and export the flamegraph.
- 2Mark key UI moments with console.time('label')/console.timeEnd('label') around suspected slow code.
- 3Run Chrome DevTools Performance panel, and record while reproducing the slowness.
- 4Look for commits in React Profiler that took >16ms (1 frame) and expand their render trees.
- 5Check for excessive re-renders using why-did-you-render or React's trace updates mode.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchReact Profiler tab in React DevTools (timeline + flamechart export)
- searchChrome DevTools Performance panel (CPU flamechart, JS heap)
- searchsrc/hooks/ and src/components/ for custom hooks/state mutation
- searchwhy-did-you-render logs in browser console
- searchNetwork and Performance tabs to correlate UI lag with API calls
- searchuseEffect/useMemo dependency arrays for stale or missing dependencies
Practical causes, not theory. These are the things you will actually find.
- warningMissing React.memo or useMemo causing large lists to re-render
- warningKey props not stable, triggering remounts
- warningExpensive calculations inline in render() without memoization
- warningRedux or context state updates propagating through too many components
- warningUncontrolled timers or polling in useEffect firing on each render
- warningLarge inline objects/functions as props breaking memoization
Concrete fix directions. Pick the one that matches your root cause.
- buildApply React.memo or useMemo/useCallback to expensive components or computations
- buildStabilize key props using useId or persistent IDs
- buildMove heavy calculations outside render unless actually dynamic
- buildRestructure context or Redux slices to minimize update blast radius
- buildThrottle or debounce rapid updates from input events or network polling
A fix you cannot prove is a guess. Close the loop.
- verifiedRepeat profiler trace and confirm total commit times drop below 16ms
- verifiedCheck input latency with Chrome DevTools Interaction to Next Paint (INP)
- verifiedRun Lighthouse and confirm TTI and TBT scores improve
- verifiedValidate that FPS during scroll or animations increases >55
- verifiedSmoke-test UI flows for regressions in functionality after optimizations
Things that make this bug worse or harder to find.
- warningBlindly memoizing everything, which can make debugging harder and hide other bugs
- warningIgnoring profiler warnings about unnecessary renders
- warningAssuming fast network = fast UI—always check actual render cost
- warningMissing impact of derived state or selector inefficiencies
- warningForgetting to profile production builds, not just dev mode
UI Lag in React List Rendering Reveals Deep Memoization Flaw
Timeline
- 10:00Initial bug report: search results UI stutters on every keystroke.
- 10:20Chrome DevTools shows no network or CPU spikes, but FPS drops to 35.
- 10:30React Profiler reveals whole list component re-rendering on each input.
- 10:40why-did-you-render logs infinite message on <ResultRow>.
- 10:45Finds props.content created as a new array on every render (fails shallow memo check).
- 11:00Wraps content prop in useMemo and adds React.memo to ResultRow.
- 11:10Profiler now shows only changed row renders; FPS stable at 58–60.
Our product search page felt janky, but only when users typed quickly into the filter box. The backend responded in under 90ms, so initially I blamed DOM layout or network.
Profiling with React DevTools showed the entire list component and every row re-rendering on every keystroke, even though search only changed a subset of results. The real clue came when why-did-you-render flagged the ResultRow component re-rendering due to a new content array being created each time.
I memoized content using useMemo and switched ResultRow to React.memo. The fix was immediate: only visible rows re-rendered, input lag disappeared, and our performance metrics improved by 45%. Next time, I’ll default to profiling traces before tweaking code blindly.
Root cause
ResultRow received a new (non-memoized) content prop every render, so memoization failed and all rows re-rendered.
The fix
Memoized the props using useMemo, then wrapped ResultRow in React.memo.
The lesson
Always check which props are breaking memoization; new array/object instances can destroy performance even in well-structured code.
A flamegraph isn't just a pretty picture—it's a map of exactly what React did during each commit and why. Look for wide blocks on the timeline: these are your worst offenders. If a parent render block contains dozens of child components, expand them and check which ones take the most time.
Use the 'Why did this render?' feature (right-click a component in Profiler) to see which props or state changed. Always anchor your investigation on the actual timeline, not on code guesses.
Never trust dev build performance: React's development warnings and validation slow things down and can mask real-world hot spots. Use the production build (`npm run build`) and serve it using a static server (`npx serve build`).
Repeat all traces and verify in your users’ real browsers, especially on less powerful devices. Many micro-bottlenecks only add up to UI jank under production conditions.
Install why-did-you-render and configure it on your root component. Watch your browser console for spammy logs: they're gold. Each log tells you if a component re-rendered needlessly and why.
You’ll often find root-cause issues like: unstable function props, deeply nested Redux selectors, or components with implicit dependencies that aren’t memoized.
Every context or Redux update re-triggers subscribed components. If your selector isn't optimized (e.g., by using reselect or memoizing returned values), a small state change can re-render half your app.
Group state and context by update frequency and consumer locality. Split large providers and use selectors that return the smallest possible shape.
Frequently asked questions
Does React Profiler impact performance during profiling?
Yes, but only while recording. The overhead is negligible for short traces and doesn't affect production. Don’t leave it running in background.
Is it always better to memoize components to fix re-renders?
No. Memoization adds complexity and memory cost. Only memoize components that are expensive to render or get non-primitive props.
Why does my app still feel slow with fast API responses?
UI lag often comes from rendering, not fetching. Always profile both network and React commit times—don't assume they correlate.
What’s the threshold for a 'slow' commit?
Any React commit over 16ms risks causing jank (missed animation frame). Target commit times of 8–12ms for user-visible updates.