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 analysisBundle 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.