reactperformancefrontendtypescript

React Performance Patterns I Use Daily

Practical performance optimization patterns for React applications that I've learned from building large-scale apps

October 20, 2025·3 min read

React Performance Patterns I Use Daily

After working on several large React applications, I've developed a toolkit of performance patterns that I reach for regularly. Here are the ones that have made the biggest impact.

1. Memoization Done Right

The key is knowing when to memoize, not just how:

// ❌ Unnecessary memoization
const SimpleButton = memo(({ onClick, label }) => (
  <button onClick={onClick}>{label}</button>
))

// ✅ Beneficial memoization - expensive render
const ExpensiveList = memo(({ items }) => (
  <ul>
    {items.map(item => (
      <ComplexListItem key={item.id} {...item} />
    ))}
  </ul>
))

2. Stable References with useCallback

function SearchComponent() {
  const [query, setQuery] = useState('')
  
  // ✅ Stable reference prevents child re-renders
  const handleSearch = useCallback((value: string) => {
    setQuery(value)
  }, [])
  
  return <SearchInput onSearch={handleSearch} />
}

3. State Colocation

Keep state as close to where it's used as possible:

// ❌ State too high in the tree
function App() {
  const [isTooltipOpen, setIsTooltipOpen] = useState(false)
  return (
    <Layout>
      <Header />
      <Content>
        <Button tooltip={isTooltipOpen} setTooltip={setIsTooltipOpen} />
      </Content>
    </Layout>
  )
}

// ✅ State colocated with usage
function Button() {
  const [isTooltipOpen, setIsTooltipOpen] = useState(false)
  return (
    <button onMouseEnter={() => setIsTooltipOpen(true)}>
      {isTooltipOpen && <Tooltip />}
    </button>
  )
}

4. Virtualization for Long Lists

For lists with hundreds of items, virtualization is essential:

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

function VirtualList({ items }) {
  const parentRef = useRef(null)
  
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
  })
  
  return (
    <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize() }}>
        {virtualizer.getVirtualItems().map(virtualRow => (
          <div key={virtualRow.key} style={{
            position: 'absolute',
            top: virtualRow.start,
            height: virtualRow.size,
          }}>
            {items[virtualRow.index].name}
          </div>
        ))}
      </div>
    </div>
  )
}

5. Lazy Loading Components

Split your bundle and load components on demand:

import { lazy, Suspense } from 'react'

const HeavyChart = lazy(() => import('./HeavyChart'))

function Dashboard() {
  return (
    <div>
      <Header />
      <Suspense fallback={<ChartSkeleton />}>
        <HeavyChart />
      </Suspense>
    </div>
  )
}

Measuring Performance

Always measure before optimizing. Use React DevTools Profiler and these custom hooks:

function useRenderCount(componentName: string) {
  const renderCount = useRef(0)
  
  useEffect(() => {
    renderCount.current += 1
    console.log(`${componentName} rendered ${renderCount.current} times`)
  })
}

Key Takeaways

  1. Don't optimize prematurely - measure first
  2. Memoization has a cost - use it wisely
  3. State colocation is often the best optimization
  4. Virtualize long lists
  5. Code-split at route boundaries

Performance optimization is an ongoing process. Start with the patterns that give you the biggest wins for your specific use case.