React Performance Rules

Rule

Scope: React and Next.js apps. Pass to daniel-product-engineer, performance-engineer, and lee-nextjs-engineer during audits.

Memoization Defeats

Inline props on React.memo'd components silently defeat memoization. Every render creates a new reference:

  • MUST NOTPass inline functions as props to memo'd children (onClick={() => doThing()})
  • MUST NOTPass inline objects/arrays as props (style={{ color: 'red' }}, items={[a, b]})
  • MUST NOTPass inline JSX as props (icon={})
  • FIX: Hoist to module-level constants, use useCallback/useMemo, or accept primitives instead

Bundle Size

  • MUSTImport m + LazyMotion from motion/react instead of full motion export — saves ~30kb
  • MUSTLazy-load heavy libraries with React.lazy or next/dynamic:
    • Chart libraries: chart.js, recharts, nivo, visx
    • 3D: three.js, @react-three/fiber
    • Data: d3, xlsx, pdf-lib
    • Editors: monaco-editor, codemirror, tiptap, lexical
  • MUSTImport from source modules, not barrel/index files — barrel imports defeat tree-shaking
    • Wrong: import { Button } from '@/components'
    • Right: import { Button } from '@/components/button'
  • MUSTUse lodash/functionName instead of full import _ from 'lodash'
  • SHOULDReplace moment.js with date-fns or dayjs (moment ships ~300kb)

Iteration Anti-Patterns

  • SHOULDCombine .filter().map() into single .reduce() or for...of — avoids iterating twice
  • SHOULDUse array.toSorted() (ES2023) instead of [...array].sort() — clearer intent, same result
  • MUST NOTUse arr[arr.sort()[0]] to find min/max — use Math.min(...arr) / Math.max(...arr)
  • SHOULDHoist new RegExp() out of loops to module-level constants

Object & State Hot Paths

  • SHOULDAvoid deep object nesting (4+ levels) in frequently-accessed data — flatten for faster reads
  • SHOULDCache repeated localStorage.getItem() / sessionStorage.getItem() calls in a variable
  • SHOULDUse startTransition to wrap non-urgent state updates that trigger expensive re-renders

Async Patterns

  • SHOULDRun independent await calls with Promise.all instead of sequential awaits:
    // Wrong — sequential, ~2x slower
    const a = await fetchA();
    const b = await fetchB();
    
    // Right — parallel
    const [a, b] = await Promise.all([fetchA(), fetchB()]);
    

Event Listeners

  • MUSTAdd { passive: true } to touchstart, touchmove, wheel, and scroll listeners — omitting it blocks scrolling performance
  • SHOULDDebounce resize and scroll handlers, or use ResizeObserver / IntersectionObserver

CSS Performance

  • MUST NOTUse transition: all — specify exact properties (transition: opacity 200ms, transform 200ms)
  • SHOULDPrefer transform and opacity for animations — they run on the compositor thread