React Performance
React Performance
React is fast by default. The virtual DOM diff algorithm is highly optimized and batches DOM updates. Most React apps do not need manual optimization — but when they do, the tools are surgical and powerful.
The golden rule: profile first, optimize second. Adding useMemo and useCallback everywhere without measurement adds complexity and can hurt performance through comparison overhead on every render.
Understanding Re-renders
A component re-renders when:
- Its own state changes
- Its parent re-renders (by default, all children re-render too)
- A context it consumes changes
Re-renders are not inherently bad — React is fast. The problem is when heavy computations or many child components re-render unnecessarily.
React.memo — Skip Child Re-renders
// Without memo: re-renders whenever parent re-renders, regardless of props
function CourseCard({ course }: { course: Course }) { ... }
// With memo: skips re-render if props have not changed by reference
const CourseCard = React.memo(function CourseCard({ course }: { course: Course }) {
return (
<div className="card">
<h3>{course.title}</h3>
<p>{course.xp} XP</p>
</div>
)
})Combine with useCallback for event handlers passed as props — otherwise a new function reference on each parent render defeats memo.
Lazy Loading with Suspense
import { lazy, Suspense } from 'react'
// These components are split into separate JS chunks
// They only download when first rendered
const AudiobookPlayer = lazy(() => import('@/components/AudiobookPlayer'))
const CertificatePage = lazy(() => import('@/app/certifications/page'))
const HeavyChart = lazy(() => import('@/components/HeavyChart'))
function CoursePage({ courseId }: { courseId: string }) {
return (
<div>
<CourseContent courseId={courseId} />
<Suspense fallback={<div className="loading-skeleton" />}>
<AudiobookPlayer courseId={courseId} />
</Suspense>
</div>
)
}In Next.js, use dynamic from 'next/dynamic' instead — it has the same API plus SSR control.
Key-based Component Reset
// Force a component to fully unmount and remount when the key changes
// All internal state resets — like destroying and rebuilding the component
function CourseViewer({ courseId }: { courseId: string }) {
return <CourseContent key={courseId} courseId={courseId} />
// ^^^^^^^^^^^
// Change courseId → old component unmounts, new one mounts fresh
}This is useful when a component has internal state that should reset when a prop changes (like a video player that should restart when the source changes).
React 18 Automatic Batching
React 18 batches all state updates in a single render — even inside async functions, promises, and setTimeout:
async function handleSubmit() {
// React 18: ALL of these are batched into ONE render
setLoading(true)
setError(null)
setData(null)
const result = await saveFormData(formData)
// Also batched — one render after the await
setLoading(false)
setData(result)
}In React 17, updates inside async code triggered separate renders. React 18 automatically batches them.
Using the DevTools Profiler
- Open React DevTools → Profiler tab
- Click "Record"
- Interact with the slow part of the UI
- Stop recording
- Inspect the flame graph — bars show time spent per component per render
- Hover a bar to see WHY the component rendered (which hook or prop changed)
Optimize only what the profiler confirms is slow.