How do you distinguish React-side vs browser-rendering bottlenecks
React DevTools Profiler measures component render time and shows what re-rendered and why. Chrome Performance panel shows main-thread tasks including style, layout, paint, composite — browser work. If Profiler shows fast renders but the user sees jank, the browser's layout/paint is the culprit. If Profiler shows slow renders, optimize React (memo, lists, context). Pair the two tools.
The split
| Cost | Tool | Symptom |
|---|---|---|
| React render time (component functions, reconciliation) | React DevTools Profiler | Long bars in flamegraph |
| Style recalc, layout, paint, composite (browser) | Chrome Performance panel | Style/Layout/Paint rows |
| Network / image decode / GPU | Chrome Performance + Lighthouse | Specific resource phases |
| Main-thread JS that isn't React (analytics, parsers) | Chrome Performance | Long Tasks |
Workflow
- Reproduce the interaction with CPU throttle.
- Record in React DevTools Profiler first — does any component render > 16ms?
- If yes → React side. Look at "Why did this render" + commit duration.
- If no → browser side or non-React.
- Record in Chrome Performance for the same interaction.
- Look at the main-thread row.
- Long tasks > 50ms flagged.
- The bottom rows (style, layout, paint) show browser work.
- Correlate: React commit at time T. Right after it, big layout/paint? That's the DOM mutation triggering browser work.
Symptoms of each side
| Symptom | Side |
|---|---|
| Same data, slow update | Probably React (re-render fan-out) |
| Adding nodes, then scroll jank | Browser (layout / paint cost) |
| Animation stutter | Browser (composite / GPU) |
| Long input delay | Either; check Profiler first |
Common React-side fixes
- Memo + stable refs for the actual hot subtree.
- Split context fan-out.
- useDeferredValue or startTransition for non-urgent.
- Virtualize long lists.
Common browser-side fixes
- Composite-only animations (transform/opacity).
contain: layout style painton isolated subtrees.- Avoid layout thrash (batch reads/writes).
- Reduce DOM node count (virtualize).
- Optimize images (size, format).
- Reduce paint area (smaller box-shadows, simpler filters).
A practical example
User drags a sidebar.
Profiler: 0.5ms commit (cheap).
Performance: massive Layout bar (20ms) after each commit.React isn't slow; the browser is laying out a deep tree on every frame. Add will-change: width or restructure CSS so only the sidebar element relayouts, not its content.
Pitfall: profiler overhead in dev
React's Profiler measurements aren't representative of production. Always build in NODE_ENV=production for accurate timing. The profiler still helps in dev for relative costs and re-render reasons.
Interview framing
"Two tools: React DevTools Profiler for render time + 'why did this render' (the React side), and Chrome Performance panel for the browser side (style recalc, layout, paint, composite). If Profiler shows fast commits but the UI feels janky, browser work is the bottleneck — usually layout cost from big DOM mutations, paint area, or non-composite animations. If Profiler shows long renders or unexpected re-renders, optimize React: memo + stable refs, split context, virtualize. Pair the tools — they tell different stories. Always profile production builds for accurate numbers."
Follow-up questions
- •What's the Performance panel's bottom-up view useful for?
- •How do you distinguish a layout cost from a paint cost?
- •When would you use the Performance panel over the Profiler?
Common mistakes
- •Optimizing React when the browser is the bottleneck.
- •Profiling in dev and trusting numbers.
- •Ignoring layout/paint rows in Performance.
Performance considerations
- •The whole question is performance categorization.
Edge cases
- •Sync layout from JS reads forcing reflow.
- •Heavy filters / shadows causing paint storms.
- •GPU memory limits on mobile.
Real-world examples
- •Long-list scroll perf, drag interactions, animation stutter case studies.