正在加载,请稍候…

React Performance Optimization in 2026: memo, Concurrent Features, and Profiling

Practical React performance optimization guide covering React.memo, useMemo, useCallback, useTransition, Profiler API, and virtual lists to eliminate unnecessary re-renders.

The Right Mental Model for React Performance

Most React performance problems are architectural problems, not framework limitations. Before reaching for useMemo or React.memo, understand why React re-renders.

React re-renders a component when:

  1. Its state changes (useState, useReducer)
  2. Its parent re-renders and passes new props
  3. A context it subscribes to changes

The goal is not to eliminate re-renders — it is to eliminate unnecessary re-renders, and ensure that necessary re-renders are fast.

Measuring Before Optimizing

import { Profiler, ProfilerOnRenderCallback } from 'react'

const onRender: ProfilerOnRenderCallback = (
  id,
  phase,
  actualDuration,
  baseDuration,
) => {
  if (actualDuration > 16) {
    console.warn(`Slow render: ${id} took ${actualDuration.toFixed(2)}ms`)
  }
}

function App() {
  return (
    <Profiler id="ProductList" onRender={onRender}>
      <ProductList />
    </Profiler>
  )
}

Open React DevTools Profiler, record an interaction, and look for:

  • Components rendering more often than expected
  • Render durations exceeding 16ms (one frame at 60fps)

React.memo: When It Helps

React.memo prevents a component from re-rendering if its props have not changed:

// Without memo: re-renders every time parent re-renders
function ExpensiveChart({ data, title }: ChartProps) { ... }

// With memo: only re-renders when data or title actually change
const ExpensiveChart = React.memo(function ExpensiveChart({ data, title }: ChartProps) { ... })

The reference equality trap:

// Bad: memo is useless — options is a new object every render
function Parent() {
  return <Chart options={{ color: 'blue', width: 300 }} />
}

// Good: move constant objects outside the component
const CHART_OPTIONS = { color: 'blue', width: 300 }
function Parent() {
  return <Chart options={CHART_OPTIONS} />
}

useMemo: Caching Expensive Computations

import { useMemo } from 'react'

function ProductList({ products, filters, sortBy }: Props) {
  // Bad: recalculated on every render, even unrelated state changes
  const filteredProducts = products
    .filter(p => filters.every(f => f(p)))
    .sort((a, b) => compareFn(a, b, sortBy))

  // Good: only recalculates when inputs change
  const filteredProducts = useMemo(
    () => products
      .filter(p => filters.every(f => f(p)))
      .sort((a, b) => compareFn(a, b, sortBy)),
    [products, filters, sortBy]
  )

  return <ul>{filteredProducts.map(p => <ProductItem key={p.id} product={p} />)}</ul>
}

Stable context values:

function UserProfile({ userId }: Props) {
  const [theme, setTheme] = useState('light')

  // Bad: new context value object every render, all consumers re-render
  // return <UserContext.Provider value={{ userId, theme, setTheme }}>

  // Good: only creates new object when userId or theme changes
  const contextValue = useMemo(
    () => ({ userId, theme, setTheme }),
    [userId, theme]
  )

  return (
    <UserContext.Provider value={contextValue}>
      <UserDetails />
    </UserContext.Provider>
  )
}

useCallback: Stable Function References

import { useCallback, memo } from 'react'

const ExpensiveChild = memo(function ExpensiveChild({
  onAction,
}: {
  onAction: (id: string) => void
}) {
  return <div>...</div>
})

function Parent() {
  const [count, setCount] = useState(0)

  // Bad: new function reference every render — breaks memo on ExpensiveChild
  // const handleAction = (id: string) => { ... }

  // Good: stable reference — memo on ExpensiveChild works as intended
  const handleAction = useCallback((id: string) => {
    analytics.track('action', { id })
  }, [])

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <ExpensiveChild onAction={handleAction} />
    </>
  )
}

Rule: useCallback is primarily useful when passing callbacks to memoized child components.

useTransition: Keeping the UI Responsive

React Concurrent Mode lets you mark state updates as non-urgent. The UI stays responsive while React processes expensive updates in the background:

import { useState, useTransition } from 'react'

function SearchPage() {
  const [query, setQuery] = useState('')
  const [isPending, startTransition] = useTransition()
  const [results, setResults] = useState<SearchResult[]>([])

  const handleSearch = (value: string) => {
    // Immediate: update input (urgent — user is typing)
    setQuery(value)

    // Deferred: run expensive search (non-urgent — can wait)
    startTransition(() => {
      const newResults = searchIndex.query(value)
      setResults(newResults)
    })
  }

  return (
    <div>
      <input value={query} onChange={e => handleSearch(e.target.value)} />
      {isPending && <Spinner />}
      <ResultsList results={results} />
    </div>
  )
}

useDeferredValue — when you do not control the state update:

function SearchResults({ query }: { query: string }) {
  const deferredQuery = useDeferredValue(query)

  const results = useMemo(() => searchIndex.query(deferredQuery), [deferredQuery])

  return (
    <div style={{ opacity: query !== deferredQuery ? 0.7 : 1 }}>
      {results.map(r => <ResultItem key={r.id} result={r} />)}
    </div>
  )
}

Virtual Lists: Handling Large Data

Rendering 10,000 list items crashes browsers. Virtual lists only render what is visible:

import { useVirtualizer } from '@tanstack/react-virtual'
import { useRef } from 'react'

function VirtualUserList({ users }: { users: User[] }) {
  const parentRef = useRef<HTMLDivElement>(null)

  const virtualizer = useVirtualizer({
    count: users.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 72,
    overscan: 5,
  })

  return (
    <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
        {virtualizer.getVirtualItems().map(virtualRow => (
          <div
            key={virtualRow.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              transform: `translateY(${virtualRow.start}px)`,
            }}
          >
            <UserRow user={users[virtualRow.index]} />
          </div>
        ))}
      </div>
    </div>
  )
}

With virtualization, rendering 100,000 items is as fast as rendering 20.

State Colocation: The Architectural Fix

Lifting state too high causes too many re-renders:

// Bad: search state in App re-renders the entire tree on every keystroke
function App() {
  const [searchQuery, setSearchQuery] = useState('')
  return (
    <div>
      <Header />   {/* Re-renders needlessly */}
      <Sidebar />  {/* Re-renders needlessly */}
      <SearchBar query={searchQuery} onChange={setSearchQuery} />
      <SearchResults query={searchQuery} />
    </div>
  )
}

// Good: colocate state with the components that need it
function App() {
  return (
    <div>
      <Header />
      <Sidebar />
      <SearchSection /> {/* Contains its own state */}
    </div>
  )
}

function SearchSection() {
  const [searchQuery, setSearchQuery] = useState('')
  return (
    <>
      <SearchBar query={searchQuery} onChange={setSearchQuery} />
      <SearchResults query={searchQuery} />
    </>
  )
}

Context Performance Pitfalls

// Bad: every consumer re-renders when user OR theme changes
const AppContext = createContext<AppState>(defaultState)

// Good: split contexts — user consumers don't re-render on theme changes
const UserContext  = createContext<UserContextValue>(defaultUser)
const ThemeContext = createContext<ThemeContextValue>(defaultTheme)

Performance Checklist

  1. Profile first — identify the slow component in React DevTools Profiler
  2. Check render frequency — is it rendering more often than expected?
  3. Check render duration — is this render inherently slow?
  4. For high frequency: React.memo + useCallback/useMemo for stable references
  5. For slow renders: virtualize long lists, use useMemo for expensive computation, useTransition for non-urgent updates
  6. For architectural issues: colocate state, split contexts

Measure, identify the specific bottleneck, then apply the targeted fix.