Optimization Patterns and Techniques
Let's explore advanced patterns for building performant React applications.
State Colocation
Keep state close to where it's used:
jsx1// Bad - Lifting state too high causes unnecessary re-renders2function App() {3 const [searchQuery, setSearchQuery] = useState('');45 return (6 <div>7 <Header /> {/* Re-renders on every keystroke! */}8 <SearchBox query={searchQuery} setQuery={setSearchQuery} />9 <Footer /> {/* Re-renders on every keystroke! */}10 </div>11 );12}1314// Good - State stays where it's needed15function App() {16 return (17 <div>18 <Header />19 <SearchBox /> {/* Manages its own state */}20 <Footer />21 </div>22 );23}2425function SearchBox() {26 const [query, setQuery] = useState('');2728 return (29 <input30 value={query}31 onChange={e => setQuery(e.target.value)}32 placeholder="Search..."33 />34 );35}
State Splitting
Split unrelated state to minimize re-renders:
jsx1// Bad - One state object for unrelated data2function Dashboard() {3 const [state, setState] = useState({4 user: null,5 notifications: [],6 theme: 'light',7 sidebarOpen: false8 });910 // Changing theme re-renders everything that uses state11}1213// Good - Separate states for separate concerns14function Dashboard() {15 const [user, setUser] = useState(null);16 const [notifications, setNotifications] = useState([]);17 const [theme, setTheme] = useState('light');18 const [sidebarOpen, setSidebarOpen] = useState(false);1920 // Changing theme only affects theme-dependent components21}
Component Composition for Performance
Use children to avoid re-renders:
jsx1// Bad - ColorPicker re-renders on every count change2function App() {3 const [count, setCount] = useState(0);45 return (6 <div>7 <button onClick={() => setCount(c => c + 1)}>8 Count: {count}9 </button>10 <ExpensiveTree />11 </div>12 );13}1415// Good - ExpensiveTree passed as children, doesn't re-render16function App() {17 return (18 <Counter>19 <ExpensiveTree />20 </Counter>21 );22}2324function Counter({ children }) {25 const [count, setCount] = useState(0);2627 return (28 <div>29 <button onClick={() => setCount(c => c + 1)}>30 Count: {count}31 </button>32 {children} {/* Already created, won't re-render! */}33 </div>34 );35}
Why this works: children is created in App (which doesn't re-render), so the reference stays stable.
Windowing Long Lists
Use virtualization for long lists:
jsx1import { useVirtualizer } from '@tanstack/react-virtual';23function VirtualList({ items }) {4 const parentRef = useRef(null);56 const virtualizer = useVirtualizer({7 count: items.length,8 getScrollElement: () => parentRef.current,9 estimateSize: () => 50,10 overscan: 511 });1213 return (14 <div15 ref={parentRef}16 style={{ height: '400px', overflow: 'auto' }}17 >18 <div19 style={{20 height: `${virtualizer.getTotalSize()}px`,21 position: 'relative'22 }}23 >24 {virtualizer.getVirtualItems().map(virtualRow => (25 <div26 key={virtualRow.key}27 style={{28 position: 'absolute',29 top: 0,30 transform: `translateY(${virtualRow.start}px)`,31 height: `${virtualRow.size}px`32 }}33 >34 {items[virtualRow.index].name}35 </div>36 ))}37 </div>38 </div>39 );40}
Debouncing Expensive Operations
Delay expensive updates:
jsx1function SearchResults({ query }) {2 const debouncedQuery = useDebounce(query, 300);34 // Only fetch when user stops typing5 const { data } = useFetch(6 debouncedQuery ? `/api/search?q=${debouncedQuery}` : null7 );89 return <ResultsList results={data} />;10}
For input state, consider useDeferredValue:
jsx1import { useDeferredValue } from 'react';23function Search() {4 const [query, setQuery] = useState('');5 const deferredQuery = useDeferredValue(query);67 // Input stays responsive, expensive render is deferred8 return (9 <>10 <input11 value={query}12 onChange={e => setQuery(e.target.value)}13 />14 <ExpensiveResults query={deferredQuery} />15 </>16 );17}
useTransition for Non-Urgent Updates
Mark updates as non-urgent:
jsx1import { useTransition } from 'react';23function TabContainer() {4 const [tab, setTab] = useState('home');5 const [isPending, startTransition] = useTransition();67 function selectTab(nextTab) {8 startTransition(() => {9 setTab(nextTab); // Non-urgent: can be interrupted10 });11 }1213 return (14 <>15 <TabButton onClick={() => selectTab('home')}>Home</TabButton>16 <TabButton onClick={() => selectTab('posts')}>Posts</TabButton>17 <TabButton onClick={() => selectTab('contact')}>Contact</TabButton>1819 <div style={{ opacity: isPending ? 0.7 : 1 }}>20 {tab === 'home' && <Home />}21 {tab === 'posts' && <Posts />} {/* Expensive! */}22 {tab === 'contact' && <Contact />}23 </div>24 </>25 );26}
Benefits:
- UI stays responsive during expensive renders
- User can interact while transition is pending
- Shows visual feedback via
isPending
Lazy Loading Routes
Code split by route:
jsx1import { lazy, Suspense } from 'react';2import { Routes, Route } from 'react-router-dom';34const Home = lazy(() => import('./pages/Home'));5const Dashboard = lazy(() => import('./pages/Dashboard'));6const Settings = lazy(() => import('./pages/Settings'));78function App() {9 return (10 <Suspense fallback={<PageLoader />}>11 <Routes>12 <Route path="/" element={<Home />} />13 <Route path="/dashboard" element={<Dashboard />} />14 <Route path="/settings" element={<Settings />} />15 </Routes>16 </Suspense>17 );18}
Image Optimization
Load images efficiently:
jsx1// Native lazy loading2<img src="photo.jpg" loading="lazy" alt="Photo" />34// With srcset for responsive images5<img6 src="photo-800.jpg"7 srcSet="photo-400.jpg 400w, photo-800.jpg 800w, photo-1200.jpg 1200w"8 sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"9 loading="lazy"10 alt="Photo"11/>1213// Using next/image (if using Next.js)14import Image from 'next/image';1516<Image17 src="/photo.jpg"18 width={800}19 height={600}20 alt="Photo"21 placeholder="blur"22 blurDataURL="data:image/jpeg;base64,..."23/>
Memoization Strategies
When to Use memo()
jsx1// Good candidates for memo:2// 1. Pure components with expensive renders3const ExpensiveChart = memo(function Chart({ data }) {4 // Complex SVG rendering5});67// 2. Components that receive stable props8const UserAvatar = memo(function Avatar({ userId }) {9 // userId is a primitive, stable10});1112// 3. List items in long lists13const ListItem = memo(function Item({ item, onSelect }) {14 // Prevents entire list re-rendering15});
Custom Comparison
jsx1const Chart = memo(2 function Chart({ data, config }) {3 // Render chart4 },5 (prevProps, nextProps) => {6 // Custom comparison - return true to skip re-render7 return (8 prevProps.data.length === nextProps.data.length &&9 prevProps.config.type === nextProps.config.type10 );11 }12);
Context Optimization
Split context by change frequency:
jsx1// Bad - Everything re-renders when anything changes2const AppContext = createContext({ user: null, theme: 'light', cart: [] });34// Good - Separate contexts5const UserContext = createContext(null);6const ThemeContext = createContext('light');7const CartContext = createContext([]);89// Components subscribe only to what they need10function ThemeToggle() {11 const [theme, setTheme] = useContext(ThemeContext);12 // Only re-renders when theme changes13}
Web Workers for Heavy Computation
Offload CPU-intensive work:
jsx1// worker.js2self.onmessage = function(e) {3 const result = heavyComputation(e.data);4 self.postMessage(result);5};67// Component8function DataProcessor({ data }) {9 const [result, setResult] = useState(null);1011 useEffect(() => {12 const worker = new Worker(new URL('./worker.js', import.meta.url));1314 worker.onmessage = (e) => {15 setResult(e.data);16 };1718 worker.postMessage(data);1920 return () => worker.terminate();21 }, [data]);2223 return result ? <Results data={result} /> : <Loading />;24}
Profiling in Production
Use React's production profiler:
jsx1import { Profiler } from 'react';23function onRenderCallback(4 id,5 phase,6 actualDuration,7 baseDuration,8 startTime,9 commitTime10) {11 // Send to analytics12 analytics.track('react_render', {13 component: id,14 phase,15 duration: actualDuration16 });17}1819function App() {20 return (21 <Profiler id="App" onRender={onRenderCallback}>22 <MainContent />23 </Profiler>24 );25}
Summary Checklist
Before optimizing:
- Measured with React DevTools Profiler
- Identified actual bottlenecks
- Verified optimization is worth the complexity
Optimization techniques:
- State colocation - keep state close to usage
- State splitting - separate unrelated state
- Component composition - use children pattern
- React.memo() - for expensive pure components
- useMemo/useCallback - stabilize references
- useTransition - for non-urgent updates
- useDeferredValue - for derived expensive values
- Virtualization - for long lists
- Code splitting - lazy load routes/features
- Context splitting - separate by change frequency
After optimizing:
- Verified improvement with Profiler
- Tested user experience
- Code remains readable
Next, let's practice these techniques with a hands-on performance audit!