A slow React app drove away 23% of our users. Here's how we identified the bottlenecks and fixed them using tools you probably already have.
The Performance Problem
Our dashboard with 200+ components was taking 8 seconds to render. Users were complaining, and our Core Web Vitals were terrible:
- LCP: 6.2s (should be < 2.5s)
- FID: 890ms (should be < 100ms)
- CLS: 0.45 (should be < 0.1)
Step 1: React DevTools Profiler
The React DevTools Profiler is your first stop. Here's how to use it effectively:
Recording a Performance Profile
- Install React DevTools browser extension
- Open your app and navigate to the Profiler tab
- Click "Record" and perform the slow action
- Stop recording and analyze the flame graph
What we found: Three components were taking 85% of render time:
UserList
: 2.1s (rendering 500+ items)Dashboard
: 1.8s (unnecessary re-renders)Charts
: 1.3s (heavy calculations on every render)
Step 2: Fix #1 - Virtualization
Our UserList
was rendering 500 items at once. Solution: React Virtual.
123456789101112131415// Before: Rendering all itemsfunction UserList({ users }) {return ({users.map(user => ( ))})}// After: Virtualized listimport { FixedSizeList as List } from 'react-window'function UserList({ users }) {const Row = ({ index, style }) => (
Result: UserList render time dropped from 2.1s to 180ms
Step 3: Fix #2 - Prevent Unnecessary Re-renders
Our Dashboard was re-rendering every child component when any data changed.
123456789101112131415// Before: Everything re-rendersfunction Dashboard() {const [users, setUsers] = useState([])const [analytics, setAnalytics] = useState({})const [notifications, setNotifications] = useState([])return ( )}// After: Memoized componentsconst UserStats = React.memo(({ users }) => {
Step 4: Fix #3 - Expensive Calculations
Our Charts component was recalculating data on every render.
123456789101112131415// Before: Calculation on every renderfunction Charts({ rawData, filters }) {// This runs on EVERY render - expensive!const processedData = rawData.filter(item => filters.includes(item.category)).map(item => ({...item,percentage: (item.value / total) * 100,formatted: formatCurrency(item.value)})).sort((a, b) => b.value - a.value)return }// After: Memoized calculationfunction Charts({ rawData, filters }) {
Step 5: Custom Performance Monitoring
We built a simple performance monitor to track component render times in production:
123456789101112131415// Performance monitoring hookfunction usePerformanceMonitor(componentName) {useEffect(() => {const startTime = performance.now()return () => {const endTime = performance.now()const renderTime = endTime - startTime// Send to analytics in productionif (process.env.NODE_ENV === 'production' && renderTime > 100) {analytics.track('slow_component_render', {component: componentName,renderTime,url: window.location.pathname})}
Advanced Debugging Techniques
1. React DevTools Profiler Settings
Enable these settings for better debugging:
- Record why each component rendered: Shows prop/state changes
- Hide components where nothing changed: Focus on problematic components
- Record timeline: See component timing in context
2. Why-did-you-render Plugin
123456789// Install: npm install @welldone-software/why-did-you-renderimport whyDidYouRender from '@welldone-software/why-did-you-render'if (process.env.NODE_ENV === 'development') {whyDidYouRender(React, {trackAllPureComponents: true,})}// Mark components to trackMyComponent.whyDidYouRender = true
3. Browser Performance Tab
Use Chrome DevTools Performance tab to see the bigger picture:
- Open DevTools → Performance tab
- Click record and perform your action
- Look for long tasks (red triangles)
- Check if React work is blocking the main thread
Performance Testing Script
We use this script to automatically test component performance:
1234567891011121314151617181920// performance-test.jsimport { render } from '@testing-library/react'import { performance } from 'perf_hooks'function measureRenderTime(Component, props) {const iterations = 10const times = []for (let i = 0; i < iterations; i++) {const start = performance.now()render( )const end = performance.now()times.push(end - start)}const average = times.reduce((a, b) => a + b) / times.lengthconst max = Math.max(...times)return { average, max, times }}// Test component performanceconst result = measureRenderTime(UserList, { users: mockUsers })console.log(`Average: ${result.average.toFixed(2)}ms`)console.log(`Max: ${result.max.toFixed(2)}ms`)
Final Results
After applying these optimizations:
- LCP: 6.2s → 1.8s (70% improvement)
- FID: 890ms → 45ms (95% improvement)
- CLS: 0.45 → 0.08 (82% improvement)
- User retention: Increased by 31%
Performance Checklist
- Use React DevTools Profiler to identify slow components
- Implement virtualization for long lists
- Memoize expensive calculations with useMemo
- Wrap pure components with React.memo
- Monitor performance in production
- Test performance changes with automation
Performance optimization is an ongoing process. Set up monitoring, measure consistently, and fix the biggest problems first.