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 NOT: Pass inline functions as props to memo'd children (
onClick={() => doThing()}) - MUST NOT: Pass inline objects/arrays as props (
style={{ color: 'red' }},items={[a, b]}) - MUST NOT: Pass inline JSX as props (
icon={}) - FIX: Hoist to module-level constants, use
useCallback/useMemo, or accept primitives instead
Bundle Size
- MUST: Import
m+LazyMotionfrommotion/reactinstead of fullmotionexport — saves ~30kb - MUST: Lazy-load heavy libraries with
React.lazyornext/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
- MUST: Import from source modules, not barrel/index files — barrel imports defeat tree-shaking
- Wrong:
import { Button } from '@/components' - Right:
import { Button } from '@/components/button'
- Wrong:
- MUST: Use
lodash/functionNameinstead of fullimport _ from 'lodash' - SHOULD: Replace moment.js with date-fns or dayjs (moment ships ~300kb)
Iteration Anti-Patterns
- SHOULD: Combine
.filter().map()into single.reduce()orfor...of— avoids iterating twice - SHOULD: Use
array.toSorted()(ES2023) instead of[...array].sort()— clearer intent, same result - MUST NOT: Use
arr[arr.sort()[0]]to find min/max — useMath.min(...arr)/Math.max(...arr) - SHOULD: Hoist
new RegExp()out of loops to module-level constants
Object & State Hot Paths
- SHOULD: Avoid deep object nesting (4+ levels) in frequently-accessed data — flatten for faster reads
- SHOULD: Cache repeated
localStorage.getItem()/sessionStorage.getItem()calls in a variable - SHOULD: Use
startTransitionto wrap non-urgent state updates that trigger expensive re-renders
Async Patterns
- SHOULD: Run independent
awaitcalls withPromise.allinstead 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
- MUST: Add
{ passive: true }totouchstart,touchmove,wheel, andscrolllisteners — omitting it blocks scrolling performance - SHOULD: Debounce
resizeandscrollhandlers, or useResizeObserver/IntersectionObserver
CSS Performance
- MUST NOT: Use
transition: all— specify exact properties (transition: opacity 200ms, transform 200ms) - SHOULD: Prefer
transformandopacityfor animations — they run on the compositor thread