# React Best Practices **Version 1.0.0** > **Note:** > This document is for agents and LLMs to follow when maintaining, > generating, or refactoring React codebases. Humans may also find it useful. --- ## Abstract Performance optimization guide for React applications, designed for AI agents and LLMs. Contains rules across 7 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). --- ## Table of Contents 1. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL** 2. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL** 3. [Client-Side Data Fetching](#3-client-side-data-fetching) — **MEDIUM-HIGH** 4. [Re-render Optimization](#4-re-render-optimization) — **MEDIUM** 5. [Rendering Performance](#5-rendering-performance) — **MEDIUM** 6. [JavaScript Performance](#6-javascript-performance) — **LOW-MEDIUM** 7. [Advanced Patterns](#7-advanced-patterns) — **LOW** --- ## 1. Eliminating Waterfalls **Impact: CRITICAL** Waterfalls are the #1 performance killer. Each sequential await adds full network latency. ### 1.1 Defer Await Until Needed **Impact: HIGH (avoids blocking unused code paths)** Move `await` operations into the branches where they're actually used. **Incorrect: blocks both branches** ```typescript async function handleRequest(userId: string, skipProcessing: boolean) { const userData = await fetchUserData(userId) if (skipProcessing) { return { skipped: true } } return processUserData(userData) } ``` **Correct: only blocks when needed** ```typescript async function handleRequest(userId: string, skipProcessing: boolean) { if (skipProcessing) { return { skipped: true } } const userData = await fetchUserData(userId) return processUserData(userData) } ``` ### 1.2 Dependency-Based Parallelization **Impact: CRITICAL (2-10× improvement)** For operations with partial dependencies, use `better-all` to maximize parallelism. **Incorrect: profile waits for config unnecessarily** ```typescript const [user, config] = await Promise.all([ fetchUser(), fetchConfig() ]) const profile = await fetchProfile(user.id) ``` **Correct: config and profile run in parallel** ```typescript import { all } from 'better-all' const { user, config, profile } = await all({ async user() { return fetchUser() }, async config() { return fetchConfig() }, async profile() { return fetchProfile((await this.$.user).id) } }) ``` ### 1.3 Promise.all() for Independent Operations **Impact: CRITICAL (2-10× improvement)** When async operations have no interdependencies, execute them concurrently. **Incorrect: sequential execution, 3 round trips** ```typescript const user = await fetchUser() const posts = await fetchPosts() const comments = await fetchComments() ``` **Correct: parallel execution, 1 round trip** ```typescript const [user, posts, comments] = await Promise.all([ fetchUser(), fetchPosts(), fetchComments() ]) ``` --- ## 2. Bundle Size Optimization **Impact: CRITICAL** Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint. ### 2.1 Avoid Barrel File Imports **Impact: CRITICAL (200-800ms import cost, slow builds)** Import directly from source files instead of barrel files. **Incorrect: imports entire library** ```tsx import { Check, X, Menu } from 'lucide-react' // Loads 1,583 modules ``` **Correct: imports only what you need** ```tsx import Check from 'lucide-react/dist/esm/icons/check' import X from 'lucide-react/dist/esm/icons/x' import Menu from 'lucide-react/dist/esm/icons/menu' // Loads only 3 modules ``` Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`. ### 2.2 Conditional Module Loading **Impact: HIGH (loads large data only when needed)** Load large data or modules only when a feature is activated. ```tsx function AnimationPlayer({ enabled, setEnabled }) { const [frames, setFrames] = useState(null) useEffect(() => { if (enabled && !frames) { import('./animation-frames.js') .then(mod => setFrames(mod.frames)) .catch(() => setEnabled(false)) } }, [enabled, frames, setEnabled]) if (!frames) return return } ``` ### 2.3 Preload Based on User Intent **Impact: MEDIUM (reduces perceived latency)** Preload heavy bundles before they're needed. ```tsx function EditorButton({ onClick }) { const preload = () => { void import('./monaco-editor') } return ( ) } ``` --- ## 3. Client-Side Data Fetching **Impact: MEDIUM-HIGH** ### 3.1 Deduplicate Global Event Listeners **Impact: MEDIUM (prevents memory leaks and duplicate handlers)** Use a singleton pattern for global event listeners. **Incorrect: multiple listeners** ```tsx function useWindowResize(callback) { useEffect(() => { window.addEventListener('resize', callback) return () => window.removeEventListener('resize', callback) }, [callback]) } ``` **Correct: shared listener** ```tsx const listeners = new Set() let isListening = false function useWindowResize(callback) { useEffect(() => { listeners.add(callback) if (!isListening) { isListening = true window.addEventListener('resize', (e) => { listeners.forEach(fn => fn(e)) }) } return () => listeners.delete(callback) }, [callback]) } ``` ### 3.2 Use Passive Event Listeners **Impact: MEDIUM (improves scroll performance)** Use passive listeners for scroll and touch events. ```tsx useEffect(() => { const handler = (e) => { /* handle scroll */ } window.addEventListener('scroll', handler, { passive: true }) return () => window.removeEventListener('scroll', handler) }, []) ``` ### 3.3 Use SWR for Automatic Deduplication **Impact: HIGH (eliminates duplicate requests)** SWR automatically deduplicates requests to the same key. ```tsx import useSWR from 'swr' function UserProfile({ userId }) { const { data } = useSWR(`/api/users/${userId}`, fetcher) return
{data?.name}
} // Multiple components using the same key = single request ``` ### 3.4 Version and Minimize localStorage Data **Impact: MEDIUM (prevents data corruption)** Use schema versioning for localStorage. ```typescript const STORAGE_VERSION = 2 interface StoredData { version: number data: UserPreferences } function loadPreferences(): UserPreferences { const raw = localStorage.getItem('prefs') if (!raw) return defaultPreferences const stored: StoredData = JSON.parse(raw) if (stored.version !== STORAGE_VERSION) { return migrate(stored) } return stored.data } ``` --- ## 4. Re-render Optimization **Impact: MEDIUM** ### 4.1 Defer State Reads to Usage Point **Impact: MEDIUM (avoids unnecessary re-renders)** Don't subscribe to state that's only used in callbacks. **Incorrect: re-renders on every count change** ```tsx function Counter() { const count = useStore(state => state.count) const increment = useStore(state => state.increment) return } ``` **Correct: no re-renders from count changes** ```tsx function Counter() { const increment = useStore(state => state.increment) return } ``` ### 4.2 Extract to Memoized Components **Impact: MEDIUM (isolates expensive renders)** Extract expensive computations into memoized child components. ```tsx const ExpensiveList = memo(function ExpensiveList({ items }) { return items.map(item => ) }) function Parent() { const [filter, setFilter] = useState('') const items = useItems() return ( <> setFilter(e.target.value)} /> ) } ``` ### 4.3 Narrow Effect Dependencies **Impact: MEDIUM (reduces effect runs)** Use primitive dependencies instead of objects. **Incorrect: runs on every render** ```tsx useEffect(() => { fetchData(options) }, [options]) // Object reference changes every render ``` **Correct: runs only when values change** ```tsx useEffect(() => { fetchData({ page, limit }) }, [page, limit]) ``` ### 4.4 Subscribe to Derived State **Impact: MEDIUM (reduces re-renders)** Subscribe to derived booleans instead of raw values. **Incorrect: re-renders on every count change** ```tsx function Badge() { const count = useStore(state => state.notifications.length) return count > 0 ? New : null } ``` **Correct: re-renders only when visibility changes** ```tsx function Badge() { const hasNotifications = useStore(state => state.notifications.length > 0) return hasNotifications ? New : null } ``` ### 4.5 Use Functional setState Updates **Impact: MEDIUM (stable callback references)** Use functional updates to avoid dependency on current state. ```tsx // Incorrect: callback changes when count changes const increment = useCallback(() => { setCount(count + 1) }, [count]) // Correct: stable callback reference const increment = useCallback(() => { setCount(c => c + 1) }, []) ``` ### 4.6 Use Lazy State Initialization **Impact: LOW-MEDIUM (avoids expensive initial computation)** Pass a function to useState for expensive initial values. ```tsx // Incorrect: parses on every render const [data, setData] = useState(JSON.parse(localStorage.getItem('data'))) // Correct: parses only once const [data, setData] = useState(() => JSON.parse(localStorage.getItem('data'))) ``` ### 4.7 Use Transitions for Non-Urgent Updates **Impact: MEDIUM (keeps UI responsive)** Use startTransition for updates that can be deferred. ```tsx import { startTransition } from 'react' function SearchResults() { const [query, setQuery] = useState('') const [results, setResults] = useState([]) const handleChange = (e) => { setQuery(e.target.value) // Urgent: update input immediately startTransition(() => { setResults(search(e.target.value)) // Non-urgent: can be interrupted }) } return ( <> ) } ``` --- ## 5. Rendering Performance **Impact: MEDIUM** ### 5.1 Animate SVG Wrapper Instead of SVG Element **Impact: MEDIUM (avoids SVG re-parsing)** Wrap SVGs in a div and animate the wrapper. ```tsx // Incorrect: triggers SVG re-parsing ... // Correct: animates wrapper only ... ``` ### 5.2 CSS content-visibility for Long Lists **Impact: HIGH (skips off-screen rendering)** Use content-visibility to skip rendering off-screen items. ```css .list-item { content-visibility: auto; contain-intrinsic-size: 0 50px; } ``` ### 5.3 Hoist Static JSX Elements **Impact: LOW-MEDIUM (avoids recreation)** Extract static JSX outside components. ```tsx const staticIcon = function ListItem({ label }) { return (
{staticIcon} {label}
) } ``` ### 5.4 Optimize SVG Precision **Impact: LOW (reduces SVG size)** Reduce coordinate precision in SVGs. ```tsx // Before: 847 bytes // After: 324 bytes ``` ### 5.5 Use Explicit Conditional Rendering **Impact: LOW (prevents rendering bugs)** Use ternary instead of && for conditionals. ```tsx // Incorrect: renders "0" when count is 0 {count && } // Correct: renders nothing when count is 0 {count > 0 ? : null} ``` --- ## 6. JavaScript Performance **Impact: LOW-MEDIUM** ### 6.1 Batch DOM CSS Changes **Impact: MEDIUM (reduces reflows)** Group CSS changes via classes or cssText. ```typescript // Incorrect: 3 reflows element.style.width = '100px' element.style.height = '100px' element.style.margin = '10px' // Correct: 1 reflow element.style.cssText = 'width: 100px; height: 100px; margin: 10px;' ``` ### 6.2 Build Index Maps for Repeated Lookups **Impact: HIGH for large datasets** Build a Map for O(1) lookups instead of O(n) array searches. ```typescript // Incorrect: O(n) for each lookup users.find(u => u.id === targetId) // Correct: O(1) lookup const userMap = new Map(users.map(u => [u.id, u])) userMap.get(targetId) ``` ### 6.3 Cache Property Access in Loops **Impact: LOW-MEDIUM** Cache object properties accessed in loops. ```typescript // Incorrect for (let i = 0; i < items.length; i++) { process(items[i]) } // Correct const len = items.length for (let i = 0; i < len; i++) { process(items[i]) } ``` ### 6.4 Cache Repeated Function Calls **Impact: MEDIUM** Cache expensive function results. ```typescript const cache = new Map() function expensiveComputation(input) { if (cache.has(input)) return cache.get(input) const result = /* expensive work */ cache.set(input, result) return result } ``` ### 6.5 Cache Storage API Calls **Impact: MEDIUM** Cache localStorage/sessionStorage reads. ```typescript let cachedTheme = null function getTheme() { if (cachedTheme === null) { cachedTheme = localStorage.getItem('theme') || 'light' } return cachedTheme } ``` ### 6.6 Combine Multiple Array Iterations **Impact: LOW-MEDIUM** Combine filter/map into a single loop. ```typescript // Incorrect: 2 iterations const result = items .filter(item => item.active) .map(item => item.value) // Correct: 1 iteration const result = [] for (const item of items) { if (item.active) result.push(item.value) } ``` ### 6.7 Early Length Check for Array Comparisons **Impact: LOW** Check array length before expensive comparisons. ```typescript function arraysEqual(a, b) { if (a.length !== b.length) return false return a.every((val, i) => val === b[i]) } ``` ### 6.8 Early Return from Functions **Impact: LOW** Return early to avoid unnecessary work. ```typescript function processUser(user) { if (!user) return null if (!user.active) return null // Process active user } ``` ### 6.9 Hoist RegExp Creation **Impact: LOW-MEDIUM** Create RegExp outside loops. ```typescript // Incorrect: creates regex on each iteration items.forEach(item => { if (/pattern/.test(item)) { /* ... */ } }) // Correct: reuses regex const pattern = /pattern/ items.forEach(item => { if (pattern.test(item)) { /* ... */ } }) ``` ### 6.10 Use Loop for Min/Max Instead of Sort **Impact: MEDIUM for large arrays** Use a loop instead of sorting to find min/max. ```typescript // Incorrect: O(n log n) const max = items.sort((a, b) => b - a)[0] // Correct: O(n) const max = Math.max(...items) // Or for very large arrays: let max = items[0] for (let i = 1; i < items.length; i++) { if (items[i] > max) max = items[i] } ``` ### 6.11 Use Set/Map for O(1) Lookups **Impact: HIGH for repeated lookups** Use Set for membership checks, Map for key-value lookups. ```typescript // Incorrect: O(n) const isValid = validIds.includes(id) // Correct: O(1) const validIdSet = new Set(validIds) const isValid = validIdSet.has(id) ``` ### 6.12 Use toSorted() for Immutability **Impact: LOW** Use toSorted() instead of sort() to avoid mutation. ```typescript // Mutates original const sorted = items.sort((a, b) => a - b) // Returns new array const sorted = items.toSorted((a, b) => a - b) ``` --- ## 7. Advanced Patterns **Impact: LOW** ### 7.1 Store Event Handlers in Refs **Impact: LOW (avoids effect re-runs)** Store handlers in refs to avoid effect dependencies. ```tsx function useEventListener(event, handler) { const handlerRef = useRef(handler) handlerRef.current = handler useEffect(() => { const listener = (e) => handlerRef.current(e) window.addEventListener(event, listener) return () => window.removeEventListener(event, listener) }, [event]) // handler not in deps } ``` ### 7.2 useLatest for Stable Callback Refs **Impact: LOW** Create a useLatest hook for stable references. ```tsx function useLatest(value) { const ref = useRef(value) ref.current = value return ref } function useInterval(callback, delay) { const callbackRef = useLatest(callback) useEffect(() => { const id = setInterval(() => callbackRef.current(), delay) return () => clearInterval(id) }, [delay]) } ``` --- ## References 1. [https://github.com/shuding/better-all](https://github.com/shuding/better-all) 2. [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)