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
  • SHOULDUse refs for high-frequency DOM reads or interaction state that does not affect rendered output
  • SHOULDUse lazy state initialization for expensive initial values (useState(() => buildIndex(items)))
  • MUSTHydration-sensitive client state (theme, tabs, accordions, viewport-derived values, timestamps) must avoid a flash of wrong content on refresh

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()]);
    
  • SHOULDDefer non-critical awaits until after early exits so cheap invalid cases return before expensive work runs.

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
  • MUST NOTLeave will-change permanently enabled. Toggle it only around the interaction that needs layer promotion.
  • MUST NOTAnimate layout properties (width, height, top, left, margin, padding) when transform/opacity can express the motion.
  • SHOULDAvoid large animated blurs and scale-from-zero entrances on production UI.