Files
next.orly.dev/.claude/skills/react/references/performance.md
mleku 27f92336ae Add NDK skill documentation and examples
- Introduced comprehensive documentation for the Nostr Development Kit (NDK) including an overview, quick reference, and troubleshooting guide.
- Added detailed examples covering initialization, authentication, event publishing, querying, and user profile management.
- Structured the documentation to facilitate quick lookups and deep learning, based on real-world usage patterns from the Plebeian Market application.
- Created an index for examples to enhance usability and navigation.
- Bumped version to 1.0.0 to reflect the addition of this new skill set.
2025-11-06 14:34:06 +00:00

14 KiB

React Performance Optimization Guide

Overview

This guide covers performance optimization strategies for React 19 applications.

Measurement & Profiling

React DevTools Profiler

Record performance data:

  1. Open React DevTools
  2. Go to Profiler tab
  3. Click record button
  4. Interact with app
  5. Stop recording
  6. Analyze flame graph and ranked chart

Profiler Component

import { Profiler } from 'react'

const App = () => {
  const onRender = (
    id: string,
    phase: 'mount' | 'update',
    actualDuration: number,
    baseDuration: number,
    startTime: number,
    commitTime: number
  ) => {
    console.log({
      component: id,
      phase,
      actualDuration,  // Time spent rendering this update
      baseDuration     // Estimated time without memoization
    })
  }
  
  return (
    <Profiler id="App" onRender={onRender}>
      <YourApp />
    </Profiler>
  )
}

Performance Metrics

// Custom performance tracking
const startTime = performance.now()
// ... do work
const endTime = performance.now()
console.log(`Operation took ${endTime - startTime}ms`)

// React rendering metrics
import { unstable_trace as trace } from 'react'

trace('expensive-operation', async () => {
  await performExpensiveOperation()
})

Memoization Strategies

React.memo

Prevent unnecessary re-renders:

// Basic memoization
const ExpensiveComponent = memo(({ data }: Props) => {
  return <div>{processData(data)}</div>
})

// Custom comparison
const MemoizedComponent = memo(
  ({ user }: Props) => <UserCard user={user} />,
  (prevProps, nextProps) => {
    // Return true if props are equal (skip render)
    return prevProps.user.id === nextProps.user.id
  }
)

When to use:

  • Component renders often with same props
  • Rendering is expensive
  • Component receives complex prop objects

When NOT to use:

  • Props change frequently
  • Component is already fast
  • Premature optimization

useMemo

Memoize computed values:

const SortedList = ({ items, filter }: Props) => {
  // Without memoization - runs every render
  const filteredItems = items.filter(item => item.type === filter)
  const sortedItems = filteredItems.sort((a, b) => a.name.localeCompare(b.name))
  
  // With memoization - only runs when dependencies change
  const sortedFilteredItems = useMemo(() => {
    const filtered = items.filter(item => item.type === filter)
    return filtered.sort((a, b) => a.name.localeCompare(b.name))
  }, [items, filter])
  
  return (
    <ul>
      {sortedFilteredItems.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  )
}

When to use:

  • Expensive calculations (sorting, filtering large arrays)
  • Creating stable object references
  • Computed values used as dependencies

useCallback

Memoize callback functions:

const Parent = () => {
  const [count, setCount] = useState(0)
  
  // Without useCallback - new function every render
  const handleClick = () => {
    setCount(c => c + 1)
  }
  
  // With useCallback - stable function reference
  const handleClickMemo = useCallback(() => {
    setCount(c => c + 1)
  }, [])
  
  return <MemoizedChild onClick={handleClickMemo} />
}

const MemoizedChild = memo(({ onClick }: Props) => {
  return <button onClick={onClick}>Click</button>
})

When to use:

  • Passing callbacks to memoized components
  • Callback is used in dependency array
  • Callback is expensive to create

React Compiler (Automatic Optimization)

Enable React Compiler

React 19 can automatically optimize without manual memoization:

// babel.config.js
module.exports = {
  plugins: [
    ['react-compiler', {
      compilationMode: 'all',  // Optimize all components
    }]
  ]
}

Compilation Modes

{
  compilationMode: 'annotation',  // Only components with "use memo"
  compilationMode: 'all',         // All components (recommended)
  compilationMode: 'infer'        // Based on component complexity
}

Directives

// Force memoization
'use memo'
const Component = ({ data }: Props) => {
  return <div>{data}</div>
}

// Prevent memoization
'use no memo'
const SimpleComponent = ({ text }: Props) => {
  return <span>{text}</span>
}

State Management Optimization

State Colocation

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

// Bad - state too high
const App = () => {
  const [showModal, setShowModal] = useState(false)
  
  return (
    <>
      <Header />
      <Content />
      <Modal show={showModal} onClose={() => setShowModal(false)} />
    </>
  )
}

// Good - state colocated
const App = () => {
  return (
    <>
      <Header />
      <Content />
      <ModalContainer />
    </>
  )
}

const ModalContainer = () => {
  const [showModal, setShowModal] = useState(false)
  
  return <Modal show={showModal} onClose={() => setShowModal(false)} />
}

Split Context

Avoid unnecessary re-renders by splitting context:

// Bad - single context causes all consumers to re-render
const AppContext = createContext({ user, theme, settings })

// Good - split into separate contexts
const UserContext = createContext(user)
const ThemeContext = createContext(theme)
const SettingsContext = createContext(settings)

Context with useMemo

const ThemeProvider = ({ children }: Props) => {
  const [theme, setTheme] = useState('light')
  
  // Memoize context value to prevent unnecessary re-renders
  const value = useMemo(() => ({
    theme,
    setTheme
  }), [theme])
  
  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  )
}

Code Splitting & Lazy Loading

React.lazy

Split components into separate bundles:

import { lazy, Suspense } from 'react'

// Lazy load components
const Dashboard = lazy(() => import('./Dashboard'))
const Settings = lazy(() => import('./Settings'))
const Profile = lazy(() => import('./Profile'))

const App = () => {
  return (
    <Suspense fallback={<Loading />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/profile" element={<Profile />} />
      </Routes>
    </Suspense>
  )
}

Route-based Splitting

// App.tsx
const routes = [
  { path: '/', component: lazy(() => import('./pages/Home')) },
  { path: '/about', component: lazy(() => import('./pages/About')) },
  { path: '/products', component: lazy(() => import('./pages/Products')) },
]

const App = () => (
  <Suspense fallback={<PageLoader />}>
    <Routes>
      {routes.map(({ path, component: Component }) => (
        <Route key={path} path={path} element={<Component />} />
      ))}
    </Routes>
  </Suspense>
)

Component-based Splitting

// Split expensive components
const HeavyChart = lazy(() => import('./HeavyChart'))

const Dashboard = () => {
  const [showChart, setShowChart] = useState(false)
  
  return (
    <>
      <button onClick={() => setShowChart(true)}>
        Load Chart
      </button>
      {showChart && (
        <Suspense fallback={<ChartSkeleton />}>
          <HeavyChart />
        </Suspense>
      )}
    </>
  )
}

List Rendering Optimization

Keys

Always use stable, unique keys:

// Bad - index as key (causes issues on reorder/insert)
{items.map((item, index) => (
  <Item key={index} data={item} />
))}

// Good - unique ID as key
{items.map(item => (
  <Item key={item.id} data={item} />
))}

// For static lists without IDs
{items.map(item => (
  <Item key={`${item.name}-${item.category}`} data={item} />
))}

Virtualization

For long lists, render only visible items:

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

const VirtualList = ({ items }: { items: Item[] }) => {
  const parentRef = useRef<HTMLDivElement>(null)
  
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,  // Estimated item height
    overscan: 5  // Render 5 extra items above/below viewport
  })
  
  return (
    <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          position: 'relative'
        }}
      >
        {virtualizer.getVirtualItems().map(virtualItem => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualItem.size}px`,
              transform: `translateY(${virtualItem.start}px)`
            }}
          >
            <Item data={items[virtualItem.index]} />
          </div>
        ))}
      </div>
    </div>
  )
}

Pagination

const PaginatedList = ({ items }: Props) => {
  const [page, setPage] = useState(1)
  const itemsPerPage = 20
  
  const paginatedItems = useMemo(() => {
    const start = (page - 1) * itemsPerPage
    const end = start + itemsPerPage
    return items.slice(start, end)
  }, [items, page, itemsPerPage])
  
  return (
    <>
      {paginatedItems.map(item => (
        <Item key={item.id} data={item} />
      ))}
      <Pagination 
        page={page} 
        total={Math.ceil(items.length / itemsPerPage)}
        onChange={setPage}
      />
    </>
  )
}

Transitions & Concurrent Features

useTransition

Keep UI responsive during expensive updates:

const SearchPage = () => {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])
  const [isPending, startTransition] = useTransition()
  
  const handleSearch = (value: string) => {
    setQuery(value)  // Urgent - update input immediately
    
    // Non-urgent - can be interrupted
    startTransition(() => {
      const filtered = expensiveFilter(items, value)
      setResults(filtered)
    })
  }
  
  return (
    <>
      <input value={query} onChange={e => handleSearch(e.target.value)} />
      {isPending && <Spinner />}
      <ResultsList results={results} />
    </>
  )
}

useDeferredValue

Defer non-urgent renders:

const SearchPage = () => {
  const [query, setQuery] = useState('')
  const deferredQuery = useDeferredValue(query)
  
  // Input updates immediately
  // Results update with deferred value (can be interrupted)
  const results = useMemo(() => {
    return expensiveFilter(items, deferredQuery)
  }, [deferredQuery])
  
  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <ResultsList results={results} />
    </>
  )
}

Image & Asset Optimization

Lazy Load Images

const LazyImage = ({ src, alt }: Props) => {
  const [isLoaded, setIsLoaded] = useState(false)
  
  return (
    <div className="relative">
      {!isLoaded && <ImageSkeleton />}
      <img
        src={src}
        alt={alt}
        loading="lazy"  // Native lazy loading
        onLoad={() => setIsLoaded(true)}
        className={isLoaded ? 'opacity-100' : 'opacity-0'}
      />
    </div>
  )
}

Next.js Image Component

import Image from 'next/image'

const OptimizedImage = () => (
  <Image
    src="/hero.jpg"
    alt="Hero"
    width={800}
    height={600}
    priority  // Load immediately for above-fold images
    placeholder="blur"
    blurDataURL="data:image/jpeg;base64,..."
  />
)

Bundle Size Optimization

Tree Shaking

Import only what you need:

// Bad - imports entire library
import _ from 'lodash'

// Good - import only needed functions
import debounce from 'lodash/debounce'
import throttle from 'lodash/throttle'

// Even better - use native methods when possible
const debounce = (fn, delay) => {
  let timeoutId
  return (...args) => {
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => fn(...args), delay)
  }
}

Analyze Bundle

# Next.js
ANALYZE=true npm run build

# Create React App
npm install --save-dev webpack-bundle-analyzer

Dynamic Imports

// Load library only when needed
const handleExport = async () => {
  const { jsPDF } = await import('jspdf')
  const doc = new jsPDF()
  doc.save('report.pdf')
}

Common Performance Pitfalls

1. Inline Object Creation

// Bad - new object every render
<Component style={{ margin: 10 }} />

// Good - stable reference
const style = { margin: 10 }
<Component style={style} />

// Or use useMemo
const style = useMemo(() => ({ margin: 10 }), [])

2. Inline Functions

// Bad - new function every render (if child is memoized)
<MemoizedChild onClick={() => handleClick(id)} />

// Good
const handleClickMemo = useCallback(() => handleClick(id), [id])
<MemoizedChild onClick={handleClickMemo} />

3. Spreading Props

// Bad - causes re-renders even when props unchanged
<Component {...props} />

// Good - pass only needed props
<Component value={props.value} onChange={props.onChange} />

4. Large Context

// Bad - everything re-renders on any state change
const AppContext = createContext({ user, theme, cart, settings, ... })

// Good - split into focused contexts
const UserContext = createContext(user)
const ThemeContext = createContext(theme)
const CartContext = createContext(cart)

Performance Checklist

  • Measure before optimizing (use Profiler)
  • Use React DevTools to identify slow components
  • Implement code splitting for large routes
  • Lazy load below-the-fold content
  • Virtualize long lists
  • Memoize expensive calculations
  • Split large contexts
  • Colocate state close to usage
  • Use transitions for non-urgent updates
  • Optimize images and assets
  • Analyze and minimize bundle size
  • Remove console.logs in production
  • Use production build for testing
  • Monitor real-world performance metrics

References