React Performance Patterns
Profiling First
// React DevTools Profiler — always profile before optimizing
// Enable in DevTools → Profiler → Record
// Code profiling with React Profiler API
import { Profiler, type ProfilerOnRenderCallback } from 'react'
const onRender: ProfilerOnRenderCallback = (id, phase, actualDuration) => {
if (actualDuration > 16) { // > 1 frame
console.warn(`Slow render: ${id} (${phase}) took ${actualDuration.toFixed(1)}ms`)
}
}
<Profiler id="Dashboard"
<Dashboard />
</Profiler>
Memoization
When to use memo / useMemo / useCallback
// memo: stable component with expensive render and stable-ish props
const ExpensiveChart = memo(function Chart({ data }: { data: DataPoint[] }) {
// renders a big SVG chart
return <svg>...</svg>
}, (prev, next) => prev.data === next.data)
// useMemo: CPU-expensive pure computation (not just "avoid re-render")
const sortedItems = useMemo(
() => [...items].sort((a, b) => a.priority - b.priority),
[items] // only re-sort when items changes
)
// useCallback: stable function ref passed to memoized child or used as dep
const handleSelect = useCallback((id: string) => {
setSelected(id)
onSelect(id)
}, [onSelect]) // onSelect itself needs to be stable
Anti-patterns
// BAD: memoizing cheap computations (overhead > savings)
const name = useMemo(() => user.firstName + ' ' + user.lastName, [user])
// BAD: memo on component that receives new object literals every render
function Parent() {
return <Child style={{ color: 'red' }} /> // new object each render
}
// GOOD: extract stable values
const style = { color: 'red' } // module-level constant
function Parent() { return <Child style={style} /> }
State Colocation
// BAD: state too high — re-renders entire tree on every keystroke
function App() {
const [search, setSearch] = useState('') // re-renders App + all children
return <><SearchBox value={search} /><HeavyList /></>
}
// GOOD: colocate state with the components that need it
function SearchBox() {
const [search, setSearch] = useState('') // only SearchBox re-renders
return <input value={search} => setSearch(e.target.value)} />
}
Context Splitting
// BAD: one context causes all consumers to re-render on any change
const AppContext = createContext({ user, theme, cart, notifications })
// GOOD: split contexts by change frequency
const UserContext = createContext<User | null>(null)
const ThemeContext = createContext<Theme>('light')
const CartContext = createContext<Cart>({ items: [] })
Lazy Loading
import { lazy, Suspense, startTransition } from 'react'
const HeavyEditor = lazy(() => import('./HeavyEditor'))
function App() {
const [showEditor, setShowEditor] = useState(false)
// startTransition: mark as non-urgent (don't block urgent UI updates)
const openEditor = () => startTransition(() => setShowEditor(true))
return (
<>
<button Editor</button>
{showEditor && (
<Suspense fallback={<EditorSkeleton />}>
<HeavyEditor />
</Suspense>
)}
</>
)
}
Concurrent Features
// useDeferredValue: defer non-urgent rendering
function SearchResults({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query)
const isStale = query !== deferredQuery
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<Suspense fallback={<Spinner />}>
<Results query={deferredQuery} />
</Suspense>
</div>
)
}
// useTransition: mark state update as non-urgent
function TabBar() {
const [isPending, startTransition] = useTransition()
const [tab, setTab] = useState('home')
return (
<>
{tabs.map(t => (
<button
key={t}
=> startTransition(() => setTab(t))}
disabled={isPending}
>
{t}
</button>
))}
<Suspense fallback={<Spinner />}>
<TabContent tab={tab} />
</Suspense>
</>
)
}
Bundle Optimization
// Dynamic import for conditional heavy deps
async function exportToPDF() {
const { jsPDF } = await import('jspdf') // loaded only when needed
const doc = new jsPDF()
doc.save('report.pdf')
}
// Tree-shaking: named imports from specific sub-paths
import { format } from 'date-fns/format' // not 'date-fns'
import { debounce } from 'lodash-es/debounce' // not 'lodash'
Image & Asset Performance
// next/image with blur placeholder
import Image from 'next/image'
<Image
src={product.imageUrl}
alt={product.name}
width={400}
height={300}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 400px"
placeholder="blur"
blurDataURL={product.blurHash}
/>
Keys & Reconciliation
// BAD: index as key causes incorrect reconciliation on reorder/insert
{items.map((item, i) => <Item key={i} item={item} />)}
// GOOD: stable unique ID
{items.map(item => <Item key={item.id} item={item} />)}
// Intentional reset via key (new key = new component instance)
<ExpensiveForm key={userId} userId={userId} />