Back to Blog
ReactPerformanceOptimizationPatterns

React Performance Patterns

Optimize React application performance. From rendering to memory to bundle optimization.

B
Bootspring Team
Engineering
May 16, 2021
6 min read

Performance impacts user experience directly. Here are proven patterns to optimize React apps.

Avoid Unnecessary Re-renders#

1// Problem: Object created on every render 2function Parent() { 3 const [count, setCount] = useState(0); 4 5 // New object every render causes Child to re-render 6 const config = { theme: 'dark' }; 7 8 return <Child config={config} />; 9} 10 11// Solution: Memoize the object 12function Parent() { 13 const [count, setCount] = useState(0); 14 15 const config = useMemo(() => ({ theme: 'dark' }), []); 16 17 return <Child config={config} />; 18} 19 20// Solution: Move outside component 21const config = { theme: 'dark' }; 22 23function Parent() { 24 const [count, setCount] = useState(0); 25 return <Child config={config} />; 26}

Memoize Callbacks#

1// Problem: New function every render 2function SearchForm({ onSearch }) { 3 const [query, setQuery] = useState(''); 4 5 // handleSubmit is recreated every render 6 const handleSubmit = (e) => { 7 e.preventDefault(); 8 onSearch(query); 9 }; 10 11 return ( 12 <form onSubmit={handleSubmit}> 13 <input value={query} onChange={e => setQuery(e.target.value)} /> 14 <MemoizedButton onClick={handleSubmit}>Search</MemoizedButton> 15 </form> 16 ); 17} 18 19// Solution: useCallback 20function SearchForm({ onSearch }) { 21 const [query, setQuery] = useState(''); 22 23 const handleSubmit = useCallback((e) => { 24 e.preventDefault(); 25 onSearch(query); 26 }, [query, onSearch]); 27 28 return ( 29 <form onSubmit={handleSubmit}> 30 <input value={query} onChange={e => setQuery(e.target.value)} /> 31 <MemoizedButton onClick={handleSubmit}>Search</MemoizedButton> 32 </form> 33 ); 34}

List Virtualization#

1import { FixedSizeList } from 'react-window'; 2 3// Problem: Rendering thousands of items 4function SlowList({ items }) { 5 return ( 6 <ul> 7 {items.map(item => ( 8 <li key={item.id}>{item.name}</li> 9 ))} 10 </ul> 11 ); 12} 13 14// Solution: Virtualize the list 15function FastList({ items }) { 16 const Row = ({ index, style }) => ( 17 <div style={style}> 18 {items[index].name} 19 </div> 20 ); 21 22 return ( 23 <FixedSizeList 24 height={400} 25 width="100%" 26 itemCount={items.length} 27 itemSize={35} 28 > 29 {Row} 30 </FixedSizeList> 31 ); 32} 33 34// With memoized rows 35const Row = memo(({ data, index, style }) => ( 36 <div style={style}> 37 {data[index].name} 38 </div> 39)); 40 41function FastList({ items }) { 42 return ( 43 <FixedSizeList 44 height={400} 45 width="100%" 46 itemCount={items.length} 47 itemSize={35} 48 itemData={items} 49 > 50 {Row} 51 </FixedSizeList> 52 ); 53}

Debounce and Throttle#

1// Problem: API call on every keystroke 2function Search() { 3 const [query, setQuery] = useState(''); 4 const [results, setResults] = useState([]); 5 6 useEffect(() => { 7 searchAPI(query).then(setResults); 8 }, [query]); 9 10 return <input value={query} onChange={e => setQuery(e.target.value)} />; 11} 12 13// Solution: Debounce the API call 14function Search() { 15 const [query, setQuery] = useState(''); 16 const [debouncedQuery, setDebouncedQuery] = useState(''); 17 const [results, setResults] = useState([]); 18 19 // Debounce the query 20 useEffect(() => { 21 const timer = setTimeout(() => { 22 setDebouncedQuery(query); 23 }, 300); 24 25 return () => clearTimeout(timer); 26 }, [query]); 27 28 // Fetch with debounced query 29 useEffect(() => { 30 if (debouncedQuery) { 31 searchAPI(debouncedQuery).then(setResults); 32 } 33 }, [debouncedQuery]); 34 35 return <input value={query} onChange={e => setQuery(e.target.value)} />; 36} 37 38// Custom hook 39function useDebounce<T>(value: T, delay: number): T { 40 const [debouncedValue, setDebouncedValue] = useState(value); 41 42 useEffect(() => { 43 const timer = setTimeout(() => setDebouncedValue(value), delay); 44 return () => clearTimeout(timer); 45 }, [value, delay]); 46 47 return debouncedValue; 48}

Code Splitting#

1import { lazy, Suspense } from 'react'; 2 3// Split by route 4const Dashboard = lazy(() => import('./Dashboard')); 5const Settings = lazy(() => import('./Settings')); 6const Profile = lazy(() => import('./Profile')); 7 8function App() { 9 return ( 10 <Suspense fallback={<PageLoader />}> 11 <Routes> 12 <Route path="/dashboard" element={<Dashboard />} /> 13 <Route path="/settings" element={<Settings />} /> 14 <Route path="/profile" element={<Profile />} /> 15 </Routes> 16 </Suspense> 17 ); 18} 19 20// Split by feature 21function ProductPage() { 22 const [showReviews, setShowReviews] = useState(false); 23 24 const Reviews = lazy(() => import('./Reviews')); 25 26 return ( 27 <div> 28 <ProductInfo /> 29 <button onClick={() => setShowReviews(true)}>Show Reviews</button> 30 31 {showReviews && ( 32 <Suspense fallback={<ReviewsSkeleton />}> 33 <Reviews productId={id} /> 34 </Suspense> 35 )} 36 </div> 37 ); 38}

Image Optimization#

1import Image from 'next/image'; 2 3// Use Next.js Image for automatic optimization 4function Gallery({ images }) { 5 return ( 6 <div> 7 {images.map(img => ( 8 <Image 9 key={img.id} 10 src={img.url} 11 alt={img.alt} 12 width={400} 13 height={300} 14 loading="lazy" 15 placeholder="blur" 16 blurDataURL={img.blurUrl} 17 /> 18 ))} 19 </div> 20 ); 21} 22 23// Native lazy loading 24function NativeGallery({ images }) { 25 return ( 26 <div> 27 {images.map(img => ( 28 <img 29 key={img.id} 30 src={img.url} 31 alt={img.alt} 32 loading="lazy" 33 decoding="async" 34 /> 35 ))} 36 </div> 37 ); 38}

State Colocation#

1// Problem: State too high in tree 2function App() { 3 const [searchQuery, setSearchQuery] = useState(''); 4 const [selectedItem, setSelectedItem] = useState(null); 5 6 return ( 7 <div> 8 <Header /> 9 <Sidebar /> 10 {/* Only SearchForm uses searchQuery */} 11 <SearchForm query={searchQuery} setQuery={setSearchQuery} /> 12 <ItemList selectedItem={selectedItem} setSelectedItem={setSelectedItem} /> 13 </div> 14 ); 15} 16 17// Solution: Colocate state with usage 18function App() { 19 return ( 20 <div> 21 <Header /> 22 <Sidebar /> 23 <SearchForm /> {/* Manages its own state */} 24 <ItemList /> {/* Manages its own state */} 25 </div> 26 ); 27} 28 29function SearchForm() { 30 const [query, setQuery] = useState(''); 31 32 return ( 33 <input value={query} onChange={e => setQuery(e.target.value)} /> 34 ); 35}

Avoid Layout Thrashing#

1// Problem: Multiple DOM reads/writes 2function resizeElements() { 3 elements.forEach(el => { 4 const height = el.offsetHeight; // Read 5 el.style.height = height + 10 + 'px'; // Write 6 }); 7} 8 9// Solution: Batch reads, then writes 10function resizeElements() { 11 // Read phase 12 const heights = elements.map(el => el.offsetHeight); 13 14 // Write phase 15 elements.forEach((el, i) => { 16 el.style.height = heights[i] + 10 + 'px'; 17 }); 18} 19 20// In React, use refs and useLayoutEffect 21function Component() { 22 const ref = useRef<HTMLDivElement>(null); 23 24 useLayoutEffect(() => { 25 if (ref.current) { 26 // Batched DOM operations 27 const height = ref.current.offsetHeight; 28 ref.current.style.minHeight = `${height}px`; 29 } 30 }, []); 31 32 return <div ref={ref}>Content</div>; 33}

Profiling#

1import { Profiler } from 'react'; 2 3function onRenderCallback( 4 id: string, 5 phase: 'mount' | 'update', 6 actualDuration: number, 7 baseDuration: number, 8 startTime: number, 9 commitTime: number 10) { 11 console.log({ 12 id, 13 phase, 14 actualDuration, 15 baseDuration, 16 }); 17} 18 19function App() { 20 return ( 21 <Profiler id="App" onRender={onRenderCallback}> 22 <MyComponent /> 23 </Profiler> 24 ); 25} 26 27// Use React DevTools Profiler for visual analysis 28// Chrome DevTools Performance tab for runtime analysis

Bundle Optimization#

1// next.config.js 2module.exports = { 3 // Analyze bundle 4 webpack: (config, { isServer }) => { 5 if (process.env.ANALYZE) { 6 const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 7 config.plugins.push( 8 new BundleAnalyzerPlugin({ 9 analyzerMode: 'static', 10 openAnalyzer: true, 11 }) 12 ); 13 } 14 return config; 15 }, 16}; 17 18// Import only what you need 19// Bad 20import _ from 'lodash'; 21_.debounce(fn, 300); 22 23// Good 24import debounce from 'lodash/debounce'; 25debounce(fn, 300); 26 27// Or use lodash-es with tree shaking 28import { debounce } from 'lodash-es';

Best Practices#

Rendering: ✓ Use React.memo for expensive components ✓ Memoize callbacks and objects ✓ Keep state as local as possible ✓ Use keys properly in lists Data: ✓ Debounce user input ✓ Virtualize long lists ✓ Paginate or infinite scroll ✓ Cache API responses Bundle: ✓ Code split by route ✓ Lazy load heavy components ✓ Tree shake imports ✓ Analyze bundle regularly

Conclusion#

React performance optimization focuses on minimizing unnecessary work: re-renders, network requests, and bundle size. Use profiling tools to identify bottlenecks, apply memoization strategically, and code split to reduce initial load. Most importantly, measure before and after optimizing to ensure real improvements.

Share this article

Help spread the word about Bootspring