JSTAcademy
0 XP
Dashboard
Crash Courses
React Performance
11 min
PhD+200 XP
Crash Courses · PhD

React Performance

Identify and fix performance issues with memoization, virtualization, and code splitting.
11 min read+200 XP on completionCert: React Crash Course
Tap any word in the text below to start reading from there.

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:

  1. Its own state changes
  2. Its parent re-renders (by default, all children re-render too)
  3. 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

  1. Open React DevTools → Profiler tab
  2. Click "Record"
  3. Interact with the slow part of the UI
  4. Stop recording
  5. Inspect the flame graph — bars show time spent per component per render
  6. Hover a bar to see WHY the component rendered (which hook or prop changed)

Optimize only what the profiler confirms is slow.

0%