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.

Core Web Vitals

  • MUST NOTHide an LCP candidate (hero image, headline) behind hydration or begin its entrance animation at opacity: 0 — the LCP timestamp is when the element renders visibly, so an entrance animation on the hero silently destroys the metric
  • MUST NOTLazy-load above-the-fold content or the LCP image
  • SHOULDTriage web-vitals work in impact order: LCP, then INP, then CLS, then transferred bytes and cache efficiency
  • MUSTSet immutable/one-year cache headers only on content-hashed or versioned asset URLs. If the release process doesn't rename an asset when its contents change, its URL must not be immutable.
  • SHOULDBatch browser geometry reads and writes with requestAnimationFrame — interleaved reads and writes force synchronous layout (layout thrashing)
  • MUSTRespect prefers-reduced-motion in both CSS and JavaScript-driven animation

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.