Vercel React Best Practices
Comprehensive performance optimization guide for React and Next.js from Vercel Engineering. 45+ rules across 8 categories: eliminating waterfalls, bundle size, server-side performance, client data fetching, re-renders, rendering, JavaScript micro-optimizations, and advanced patterns. Each rule includes editable code examples with live preview.
These practices are ordered by impact: critical ones (waterfalls and bundle size) directly affect load time and user experience; medium-priority rules (re-renders, rendering) improve responsiveness; advanced ones polish edge cases. Use the examples in the editor: edit, run, and compare behavior.
Categories by priority
All code blocks are editableโpress Run โ/Ctrl+Enter to see the preview.
๐ 1. Eliminating Waterfalls
๐จ Waterfalls are the #1 performance killer in web apps โ and they're hiding everywhere! Each sequential await adds a full network round-trip, turning what should be a snappy 100ms load into a painful multi-second crawl. ๐ก The fix? Fire independent fetches in parallel with Promise.all(), defer await until you actually need the result, and use Suspense boundaries to stream content as it loads.
๐ด Impact: CRITICAL โ These are the highest-ROI optimizations you can make. Fix waterfalls first and you'll see the biggest performance gains!
๐ In this section: Promise.all() parallelization โข Deferred awaits โข Dependency-based parallel execution โข API route optimization โข Suspense streaming
Promise.all() for Independent Operations
๐โโ๏ธ Run both panels to see the difference: the bad example waits for each fetch one after another (like standing in 3 separate lines ๐ฉ); the good example runs them all at once! โก
What to notice
- Three sequential awaits: each call waits for the previous to finish.
- Total time = sum of all latencies (e.g. 300ms + 300ms + 300ms).
// โ Sequential: each await blocks the next (waterfall)const fetchUser = () => new Promise(r => setTimeout(() => r({ name: 'User' }), 400));const fetchPosts = () => new Promise(r => setTimeout(() => r([{ id: 1 }]), 400));const fetchComments = () => new Promise(r => setTimeout(() => r([{ id: 1 }]), 400));function App() {const [result, setResult] = React.useState(null);const [timing, setTiming] = React.useState('');React.useEffect(() => {const start = performance.now();(async () => {const user = await fetchUser();const posts = await fetchPosts();const comments = await fetchComments();setResult({ user, posts, comments });setTiming(((performance.now() - start) / 1000).toFixed(2) + 's');})();}, []);return <div>Sequential: {timing || 'loading...'} (3 ร 400ms)</div>;}export default App;
What to notice
- Promise.all() starts all fetches at once.
- Total time = slowest single request (e.g. ~300ms).
// โ Parallel: all fetches start togetherconst fetchUser = () => new Promise(r => setTimeout(() => r({ name: 'User' }), 400));const fetchPosts = () => new Promise(r => setTimeout(() => r([{ id: 1 }]), 400));const fetchComments = () => new Promise(r => setTimeout(() => r([{ id: 1 }]), 400));function App() {const [result, setResult] = React.useState(null);const [timing, setTiming] = React.useState('');React.useEffect(() => {const start = performance.now();Promise.all([fetchUser(), fetchPosts(), fetchComments()]).then(([user, posts, comments]) => {setResult({ user, posts, comments });setTiming(((performance.now() - start) / 1000).toFixed(2) + 's');});}, []);return <div>Parallel: {timing || 'loading...'} (~400ms)</div>;}export default App;
Defer Await Until Needed
What to notice
- The await runs before we check skipProcessing, so we always fetch userData even when we're going to skipโwasting a round-trip.
- When skipProcessing is true, we still pay the latency and server cost of fetchUserData. Move the guard above any async work.
async function handleRequest(userId: string, skipProcessing: boolean) {const userData = await fetchUserData(userId)if (skipProcessing) return { skipped: true }return processUserData(userData)}
What to notice
- Return early before any await: if we're going to skip, we never call fetchUserData, so zero wasted latency or server work.
- We only fetch when we actually need the data. This pattern is essential for fast API routes and server components.
async function handleRequest(userId: string, skipProcessing: boolean) {if (skipProcessing) return { skipped: true }const userData = await fetchUserData(userId)return processUserData(userData)}
Dependency-Based Parallelization (better-all)
What to notice
- We wait for user and config first, then we wait for profile (which depends on user.id). Two sequential round-trips.
- fetchProfile cannot start until user is ready, so total time is the sum of both phases instead of overlapping work.
const [user, config] = await Promise.all([fetchUser(), fetchConfig()])const profile = await fetchProfile(user.id)
What to notice
- Independent tasks (user, config) run in parallel; profile starts as soon as user is available, so dependencies are resolved without blocking.
- user and config fire immediately; profile runs in parallel with the tail of the first phase. Fewer round-trips and better latency.
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)}})
Prevent Waterfall Chains in API Routes
What to notice
- Each await blocks the next: session โ config โ data in sequence.
- Total latency = sum of auth + fetchConfig + fetchData.
export async function GET(request: Request) {const session = await auth()const config = await fetchConfig()const data = await fetchData(session.user.id)return Response.json({ data, config })}
What to notice
- Start auth and fetchConfig immediately; only await session before fetchData (it depends on session).
- config loads in parallel with session; then data loads using session.user.id.
export async function GET(request: Request) {const sessionPromise = auth()const configPromise = fetchConfig()const session = await sessionPromiseconst [config, data] = await Promise.all([configPromise,fetchData(session.user.id)])return Response.json({ data, config })}
Strategic Suspense Boundaries
What to notice
- Page is async and awaits fetchData: nothing renders until data is ready.
- User sees a blank page until the slowest data loads (blocking).
async function Page() {const data = await fetchData()return (<div><div>Sidebar</div><div>Header</div><div><DataDisplay data={data} /></div><div>Footer</div></div>)}
What to notice
- Suspense wraps only the part that needs data; Sidebar, Header, Footer render immediately.
- DataDisplay fetches inside; Suspense shows Skeleton until ready (streaming).
function Page() {return (<div><div>Sidebar</div><div>Header</div><Suspense fallback={<Skeleton />}><DataDisplay /></Suspense><div>Footer</div></div>)}async function DataDisplay() {const data = await fetchData()return <div>{data.content}</div>}
๐ฆ 2. Bundle Size Optimization
๐ Every kilobyte of JavaScript must be parsed, compiled, and executed before your app becomes interactive โ that's dead time your users are staring at a blank screen! ๐ฏ Import directly from source modules (bye bye barrel files ๐), use next/dynamic for heavy components like editors and charts, defer non-critical third-party scripts, and preload on hover/focus so assets are ready the instant your user clicks.
๐ด Impact: CRITICAL โ Smaller bundles = faster Time to Interactive. Your users on slow 3G connections will thank you! ๐
๐ In this section: Barrel file avoidance โข Dynamic imports โข Third-party deferral โข Conditional module loading โข Hover/focus preloading
Avoid Barrel File Imports
import { Check, X, Menu } from 'lucide-react'import { Button, TextField } from '@mui/material'
import Check from 'lucide-react/dist/esm/icons/check'import Button from '@mui/material/Button'import TextField from '@mui/material/TextField'// Or: next.config.js experimental.optimizePackageImports: ['lucide-react', '@mui/material']
optimizePackageImports to your config instead of rewriting every import! The bundler handles it automatically. ๐ชDynamic Imports for Heavy Components
import { MonacoEditor } from './monaco-editor'function CodePanel({ code }) {return <MonacoEditor value={code} />}
import dynamic from 'next/dynamic'const MonacoEditor = dynamic(() => import('./monaco-editor').then(m => m.MonacoEditor),{ ssr: false })function CodePanel({ code }) {return <MonacoEditor value={code} />}
Defer Non-Critical Third-Party Libraries
import { Analytics } from '@vercel/analytics/react'export default function RootLayout({ children }) {return (<html><body>{children}<Analytics /></body></html>)}
import dynamic from 'next/dynamic'const Analytics = dynamic(() => import('@vercel/analytics/react').then(m => m.Analytics),{ ssr: false })export default function RootLayout({ children }) {return (<html><body>{children}<Analytics /></body></html>)}
Conditional Module Loading
function AnimationPlayer({ enabled, setEnabled }) {const [frames, setFrames] = useState(null)useEffect(() => {if (enabled && !frames && typeof window !== 'undefined') {import('./animation-frames.js').then(mod => setFrames(mod.frames)).catch(() => setEnabled(false))}}, [enabled, frames, setEnabled])if (!frames) return <div className="h-20 bg-gray-200 animate-pulse" />return <div>Canvas with {frames.length} frames</div>}
Preload on Hover/Focus
function EditorButton({ onClick }) {const preload = () => {if (typeof window !== 'undefined') void import('./monaco-editor')}return (<button onMouseEnter={preload} onFocus={preload} onClick={onClick}>Open Editor</button>)}
๐ฅ๏ธ 3. Server-Side Performance
โ๏ธ Server Components run on every single request, so inefficiencies multiply fast โ what's 10ms wasted becomes 10ms ร 1000 req/s = 10 seconds of server time burned every second! ๐ฅ Use React.cache() to deduplicate fetches within a request, LRU caches across requests, and keep RSC serialization lean. Parallelize data loading through component composition, use after() for non-blocking work, and ๐ always authenticate Server Actions โ they're public HTTP endpoints anyone can hit!
๐ Impact: HIGH โ Server optimizations scale with traffic. Small wins here pay off big under load! ๐
๐ In this section: React.cache() dedup โข LRU caching โข RSC serialization โข Parallel composition โข after() non-blocking โข Server Action auth โข Reference deduplication
Per-Request Deduplication with React.cache()
const getUser = cache(async (params: { uid: number }) => {return await db.user.findUnique({ where: { id: params.uid } })})getUser({ uid: 1 })getUser({ uid: 1 }) // Cache miss
import { cache } from 'react'export const getCurrentUser = cache(async () => {const session = await auth()if (!session?.user?.id) return nullreturn await db.user.findUnique({ where: { id: session.user.id } })})const getUser = cache(async (uid: number) => {return await db.user.findUnique({ where: { id: uid } })})getUser(1)getUser(1) // Cache hit
Cross-Request LRU Caching
import { LRUCache } from 'lru-cache'const cache = new LRUCache<string, any>({max: 1000,ttl: 5 * 60 * 1000 // 5 minutes})export async function getUser(id: string) {const cached = cache.get(id)if (cached) return cachedconst user = await db.user.findUnique({ where: { id } })cache.set(id, user)return user}
Minimize Serialization at RSC Boundaries
async function Page() {const user = await fetchUser()return <Profile user={user} />}'use client'function Profile({ user }) {return <div>{user.name}</div>}
async function Page() {const user = await fetchUser()return <Profile name={user.name} />}'use client'function Profile({ name }) {return <div>{name}</div>}
Parallel Data Fetching with Composition
export default async function Page() {const header = await fetchHeader()return (<div><div>{header}</div><Sidebar /></div>)}async function Sidebar() {const items = await fetchSidebarItems()return <nav>{items.map(i => <span key={i}>{i}</span>)}</nav>}
async function Header() {const data = await fetchHeader()return <div>{data}</div>}async function Sidebar() {const items = await fetchSidebarItems()return <nav>{items.map(i => <span key={i}>{i}</span>)}</nav>}export default function Page() {return (<div><Header /><Sidebar /></div>)}
Use after() for Non-Blocking Operations
export async function POST(request: Request) {await updateDatabase(request)await logUserAction({ userAgent: request.headers.get('user-agent') })return new Response(JSON.stringify({ status: 'success' }))}
import { after } from 'next/server'import { headers, cookies } from 'next/headers'export async function POST(request: Request) {await updateDatabase(request)after(async () => {const userAgent = (await headers()).get('user-agent') || 'unknown'const sessionCookie = (await cookies()).get('session-id')?.valuelogUserAction({ sessionCookie, userAgent })})return new Response(JSON.stringify({ status: 'success' }))}
Authenticate Server Actions Like API Routes (CRITICAL)
๐ Server Actions are public HTTP endpoints โ anyone can call them with a POST request, even without your UI! Think of them like unlocked API routes. ๐ช You MUST verify authentication and authorization inside every Server Action. Never assume the caller is who your UI thinks they are โ that's how data breaches happen! ๐ฑ
What to notice
- No auth check: anyone with the endpoint URL can delete any project.
- Trusts that userId comes from an authenticated user โ it doesn't.
'use server'export async function deleteProject(projectId: string) {// โ No authentication or authorization check!await db.project.delete({ where: { id: projectId } })revalidatePath('/projects')}
What to notice
- Verifies session inside the action โ unauthenticated callers get rejected.
- Checks authorization: only the project owner can delete their project.
'use server'import { auth } from '@/lib/auth'export async function deleteProject(projectId: string) {// โ Verify authenticationconst session = await auth()if (!session?.user?.id) {throw new Error('Unauthorized')}// โ Verify authorization (user owns this project)const project = await db.project.findUnique({where: { id: projectId }})if (project?.ownerId !== session.user.id) {throw new Error('Forbidden')}await db.project.delete({ where: { id: projectId } })revalidatePath('/projects')}
requireAuth() wrapper that throws on unauthenticated access. Use it as the first line of every Server Action!Avoid Duplicate Serialization in RSC Props (LOW)
๐ React Server Components serialize props by reference, not value. So when you call .toSorted() or .filter() twice, you create two different references โ even though the content is identical! ๐คฏ RSC will serialize the same data twice. Sort once, assign to a variable, pass the same reference everywhere.
What to notice
- .toSorted() creates a new array reference โ RSC serializes it as a separate copy.
- Both <Sidebar> and <Main> receive the same data but serialized twice.
async function Page() {const items = await fetchItems()return (<>{/* โ .toSorted() creates a new reference each call */}<Sidebar items={items.toSorted(byDate)} /><Main items={items.toSorted(byDate)} /></>)}
What to notice
- Sort once, store in a variable, pass the same reference to both components.
- RSC deduplicates by reference โ data is serialized only once.
async function Page() {const items = await fetchItems()// โ Sort once, reuse the same referenceconst sorted = items.toSorted(byDate)return (<><Sidebar items={sorted} /><Main items={sorted} /></>)}
๐ 4. Client-Side Data Fetching
๐ Fetching data on the client without a caching layer? You're asking for trouble โ redundant network requests, stale data, and gnarly race conditions! ๐ฑ Use SWR or React Query for automatic request deduplication, smart caching, and background revalidation. Deduplicate global event listeners (memory leaks are no joke ๐), use passive listeners for scroll/touch to keep the main thread free, and version your localStorage schema so deployments don't corrupt user data.
๐ก Impact: MEDIUM-HIGH โ Smart data fetching eliminates wasted requests and keeps your UI feeling fresh and responsive! โจ
๐ In this section: SWR deduplication โข Event listener management โข Passive listeners โข localStorage versioning
Use SWR for Automatic Deduplication
function UserList() {const [users, setUsers] = useState([])useEffect(() => {fetch('/api/users').then(r => r.json()).then(setUsers)}, [])return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>}
import useSWR from 'swr'function UserList() {const { data: users } = useSWR('/api/users', fetcher)return <ul>{(users||[]).map(u => <li key={u.id}>{u.name}</li>)}</ul>}
Deduplicate Global Event Listeners
import useSWRSubscription from 'swr/subscription'const keyCallbacks = new Map()function useKeyboardShortcut(key, callback) {useEffect(() => {if (!keyCallbacks.has(key)) keyCallbacks.set(key, new Set())keyCallbacks.get(key).add(callback)return () => {const set = keyCallbacks.get(key)if (set) { set.delete(callback); if (set.size === 0) keyCallbacks.delete(key) }}}, [key, callback])useSWRSubscription('global-keydown', () => {const handler = (e) => {if (e.metaKey && keyCallbacks.has(e.key))keyCallbacks.get(e.key).forEach(cb => cb())}window.addEventListener('keydown', handler)return () => window.removeEventListener('keydown', handler)})}
Passive Event Listeners for Scroll
document.addEventListener('touchstart', handleTouch)document.addEventListener('wheel', handleWheel)
document.addEventListener('touchstart', handleTouch, { passive: true })document.addEventListener('wheel', handleWheel, { passive: true })
Version and Minimize localStorage
๐๏ธ Version your localStorage keys and store only what you need! Prevents schema conflicts across deployments and stops you from accidentally caching sensitive data. Always wrap in try-catch โ localStorage throws in incognito mode and when quota is exceeded! ๐ฅ
// No version, stores everything, no error handlinglocalStorage.setItem('userConfig', JSON.stringify(fullUserObject))const data = localStorage.getItem('userConfig')
const VERSION = 'v2'function saveConfig(config: { theme: string; language: string }) {try {localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))} catch {// Throws in incognito/private browsing, quota exceeded, or disabled}}function loadConfig() {try {const data = localStorage.getItem(`userConfig:${VERSION}`)return data ? JSON.parse(data) : null} catch {return null}}// Migration from v1 to v2function migrate() {try {const v1 = localStorage.getItem('userConfig:v1')if (v1) {const old = JSON.parse(v1)saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })localStorage.removeItem('userConfig:v1')}} catch {}}// Store minimal fields from server responsesfunction cachePrefs(user: FullUser) {try {localStorage.setItem('prefs:v1', JSON.stringify({theme: user.preferences.theme,notifications: user.preferences.notifications}))} catch {}}
โ Benefits: Schema evolution via versioning โข ๐ฆ Reduced storage size โข ๐ Prevents storing tokens/PII/internal flags
โก 5. Re-render Optimization
๐ฅ Unnecessary re-renders are React's silent performance thief! Every re-render walks the component tree, diffs virtual DOM, and can trigger expensive computations โ even when nothing visible changed. ๐ง Learn to derive state during render, use useRef for transient values, hoist default props, keep effect deps narrow, and wrap non-urgent updates in startTransition to keep your UI butter-smooth.
๐ต Impact: MEDIUM โ Death by a thousand re-renders! Each one is cheap, but they add up fast in complex UIs. ๐ฏ
๐ In this section: Deferred state reads โข Memoized components โข Effect dependencies โข Derived state โข Functional setState โข Lazy init โข Transitions โข Default prop hoisting โข useRef for transient values โข Event handler logic
Defer State Reads to Usage Point
function ShareButton({ chatId }) {const searchParams = useSearchParams()const handleShare = () => {shareChat(chatId, { ref: searchParams.get('ref') })}return <button onClick={handleShare}>Share</button>}
function ShareButton({ chatId }) {const handleShare = () => {const ref = new URLSearchParams(window.location.search).get('ref')shareChat(chatId, { ref })}return <button onClick={handleShare}>Share</button>}
Extract to Memoized Components
function Profile({ user, loading }) {const avatar = useMemo(() => <Avatar id={computeAvatarId(user)} />, [user])if (loading) return <div className="h-20 bg-gray-200" />return <div>{avatar}</div>}
import { memo } from 'react'const UserAvatar = memo(function UserAvatar({ user }) {const id = useMemo(() => computeAvatarId(user), [user])return <Avatar id={id} />})function Profile({ user, loading }) {if (loading) return <div className="h-20 bg-gray-200" />return <div><UserAvatar user={user} /></div>}
Interactive Example: Memoized Components
import { useState, memo, useMemo } from 'react'// โ Correct: Memoized component enables early returnconst UserAvatar = memo(function UserAvatar({ user }) {const avatarId = useMemo(() => {// Expensive computationreturn user.name.toLowerCase().replace(/\s+/g, '-')}, [user.name])return (<div style={{ padding: '1rem', background: '#1e293b', borderRadius: '8px' }}><div style={{ fontSize: '2rem', marginBottom: '0.5rem' }}>{user.avatar || '๐ค'}</div><div style={{ color: '#94a3b8' }}>ID: {avatarId}</div><div style={{ color: '#fff', fontWeight: 'bold' }}>{user.name}</div></div>)})function Profile({ user, loading }) {// Early return - no expensive computation when loadingif (loading) {return (<div style={{ padding: '1rem', background: '#334155', borderRadius: '8px' }}><div style={{ height: '20px', background: '#475569', borderRadius: '4px', marginBottom: '0.5rem' }} /><div style={{ height: '16px', background: '#475569', borderRadius: '4px', width: '60%' }} /></div>)}return (<div><UserAvatar user={user} /></div>)}export const App = () => {const [loading, setLoading] = useState(false)const [user, setUser] = useState({ name: 'John Doe', avatar: '๐จโ๐ป' })return (<div style={{ padding: '2rem', maxWidth: '400px', margin: '0 auto' }}><h2 style={{ color: '#fff', marginBottom: '1rem' }}>Memoized Component Demo</h2><div style={{ marginBottom: '1rem', display: 'flex', gap: '0.5rem' }}><buttononClick={() => setLoading(!loading)}style={{padding: '0.5rem 1rem',background: loading ? '#ef4444' : '#10b981',color: '#fff',border: 'none',borderRadius: '6px',cursor: 'pointer'}}>{loading ? 'Show Loading' : 'Show Profile'}</button><buttononClick={() => setUser({ ...user, name: user.name === 'John Doe' ? 'Jane Smith' : 'John Doe' })}style={{padding: '0.5rem 1rem',background: '#6366f1',color: '#fff',border: 'none',borderRadius: '6px',cursor: 'pointer'}}>Change User</button></div><Profile user={user} loading={loading} /><p style={{ color: '#94a3b8', fontSize: '0.875rem', marginTop: '1rem' }}>{loading? 'โ Early return - no expensive computation': 'โ Avatar computed only when needed'}</p></div>)}
Narrow Effect Dependencies
useEffect(() => { console.log(user.id) }, [user])
useEffect(() => { console.log(user.id) }, [user.id])
Subscribe to Derived State
๐ฏ Subscribe to a derived boolean instead of a continuous value! Why re-render on every single pixel of resize when you only care about "is it mobile or not"? Re-render only when the boolean flips โ that's potentially 100ร fewer renders! ๐
function Sidebar() {const width = useWindowWidth() // updates continuouslyconst isMobile = width < 768return <nav className={isMobile ? 'mobile' : 'desktop'} />}
function Sidebar() {const isMobile = useMediaQuery('(max-width: 767px)') // re-renders only when boolean changesreturn <nav className={isMobile ? 'mobile' : 'desktop'} />}
import { useState, useEffect } from 'react'// โ Correct: Subscribe to derived booleanfunction useMediaQuery(query: string) {const [matches, setMatches] = useState(() => {if (typeof window === 'undefined') return falsereturn window.matchMedia(query).matches})useEffect(() => {const media = window.matchMedia(query)const handler = (e: MediaQueryListEvent) => setMatches(e.matches)media.addEventListener('change', handler)return () => media.removeEventListener('change', handler)}, [query])return matches}// โ Wrong: Subscribe to continuous valuefunction useWindowWidth() {const [width, setWidth] = useState(() => {if (typeof window === 'undefined') return 0return window.innerWidth})useEffect(() => {const handler = () => setWidth(window.innerWidth)window.addEventListener('resize', handler)return () => window.removeEventListener('resize', handler)}, [])return width}function Sidebar() {// โ Only re-renders when crossing 767px thresholdconst isMobile = useMediaQuery('(max-width: 767px)')return (<nav style={{padding: '1rem',background: isMobile ? '#1e293b' : '#0f172a',color: '#fff',borderRadius: '8px'}}><div style={{ marginBottom: '0.5rem' }}>Mode: {isMobile ? 'Mobile' : 'Desktop'}</div><div style={{ fontSize: '0.75rem', color: '#94a3b8' }}>โ Re-renders only when boolean changes<br />โ Not on every pixel resize</div></nav>)}export const App = () => {return (<div style={{ padding: '2rem' }}><h2 style={{ color: '#fff', marginBottom: '1rem' }}>Derived State Demo</h2><p style={{ color: '#94a3b8', marginBottom: '1rem' }}>Resize the window to see the difference. The component only re-renderswhen crossing the 767px threshold, not on every pixel change.</p><Sidebar /></div>)}
Use Functional setState Updates
const addItems = useCallback((newItems) => {setItems([...items, ...newItems])}, [items])const removeItem = useCallback((id) => {setItems(items.filter(item => item.id !== id))}, [])
const addItems = useCallback((newItems) => {setItems(curr => [...curr, ...newItems])}, [])const removeItem = useCallback((id) => {setItems(curr => curr.filter(item => item.id !== id))}, [])
Interactive Example: Functional setState
import { useState, useCallback } from 'react'function TodoList() {const [items, setItems] = useState([{ id: 1, text: 'Learn React' },{ id: 2, text: 'Build app' }])// โ Correct: Functional setState - stable callbackconst addItems = useCallback((newItems) => {setItems(curr => [...curr, ...newItems])}, []) // No dependency on items!// โ Correct: Functional setState - stable callbackconst removeItem = useCallback((id) => {setItems(curr => curr.filter(item => item.id !== id))}, []) // No dependency on items!return (<div style={{ padding: '2rem', maxWidth: '500px', margin: '0 auto' }}><h2 style={{ color: '#fff', marginBottom: '1rem' }}>Todo List</h2><div style={{ marginBottom: '1rem', display: 'flex', gap: '0.5rem' }}><buttononClick={() => addItems([{ id: Date.now(), text: 'New Task ' + Date.now() }])}style={{padding: '0.5rem 1rem',background: '#10b981',color: '#fff',border: 'none',borderRadius: '6px',cursor: 'pointer'}}>Add Item</button></div><div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>{items.map(item => (<divkey={item.id}style={{padding: '0.75rem',background: '#1e293b',borderRadius: '6px',display: 'flex',justifyContent: 'space-between',alignItems: 'center'}}><span style={{ color: '#fff' }}>{item.text}</span><buttononClick={() => removeItem(item.id)}style={{padding: '0.25rem 0.75rem',background: '#ef4444',color: '#fff',border: 'none',borderRadius: '4px',cursor: 'pointer',fontSize: '0.875rem'}}>Remove</button></div>))}</div><div style={{marginTop: '1.5rem',padding: '1rem',background: '#14532d',borderRadius: '8px',fontSize: '0.875rem',color: '#86efac'}}>โ Callbacks are stable (no dependency on items)<br />โ Functional setState prevents stale closures<br />โ Better performance - callbacks don't recreate</div></div>)}export const App = () => <TodoList />
Use Lazy State Initialization
const [settings, setSettings] = useState(JSON.parse(localStorage.getItem('settings') || '{}'))
const [settings, setSettings] = useState(() => {const stored = localStorage.getItem('settings')return stored ? JSON.parse(stored) : {}})
Interactive Example: Lazy State Initialization
import { useState } from 'react'// Simulate localStorageconst mockStorage = {getItem: (key) => {if (typeof window === 'undefined') return nullconst stored = window.localStorage?.getItem(key)return stored || null},setItem: (key, value) => {if (typeof window !== 'undefined') {window.localStorage?.setItem(key, value)}}}// Initialize with mock data for demoif (typeof window !== 'undefined' && !mockStorage.getItem('settings')) {mockStorage.setItem('settings', JSON.stringify({ theme: 'dark', lang: 'en' }))}function SettingsPanel() {// โ Correct: Lazy initialization - only runs onceconst [settings, setSettings] = useState(() => {const stored = mockStorage.getItem('settings')console.log('Initializing settings (runs only once)...')return stored ? JSON.parse(stored) : { theme: 'light', lang: 'en' }})const updateSetting = (key, value) => {const newSettings = { ...settings, [key]: value }setSettings(newSettings)mockStorage.setItem('settings', JSON.stringify(newSettings))}return (<div style={{ padding: '2rem', maxWidth: '400px', margin: '0 auto' }}><h2 style={{ color: '#fff', marginBottom: '1rem' }}>Settings</h2><div style={{ marginBottom: '1rem' }}><label style={{ display: 'block', color: '#94a3b8', marginBottom: '0.5rem' }}>Theme</label><selectvalue={settings.theme}onChange={(e) => updateSetting('theme', e.target.value)}style={{width: '100%',padding: '0.5rem',background: '#1e293b',color: '#fff',border: '1px solid #334155',borderRadius: '6px'}}><option value="light">Light</option><option value="dark">Dark</option><option value="auto">Auto</option></select></div><div style={{ marginBottom: '1rem' }}><label style={{ display: 'block', color: '#94a3b8', marginBottom: '0.5rem' }}>Language</label><selectvalue={settings.lang}onChange={(e) => updateSetting('lang', e.target.value)}style={{width: '100%',padding: '0.5rem',background: '#1e293b',color: '#fff',border: '1px solid #334155',borderRadius: '6px'}}><option value="en">English</option><option value="es">Espaรฑol</option><option value="fr">Franรงais</option></select></div><div style={{padding: '1rem',background: '#14532d',borderRadius: '8px',fontSize: '0.875rem',color: '#86efac'}}>โ Lazy initialization runs only once<br />โ Expensive operations (JSON.parse) don't run on every render<br />โ Check console - initialization logs only once</div></div>)}export const App = () => <SettingsPanel />
Use Transitions for Non-Urgent Updates
useEffect(() => {const handler = () => setScrollY(window.scrollY)window.addEventListener('scroll', handler, { passive: true })return () => window.removeEventListener('scroll', handler)}, [])
import { startTransition } from 'react'useEffect(() => {const handler = () => {startTransition(() => setScrollY(window.scrollY))}window.addEventListener('scroll', handler, { passive: true })return () => window.removeEventListener('scroll', handler)}, [])
Interactive Example: Transitions for Smooth UI
import { useState, useEffect, startTransition } from 'react'function ScrollTracker() {const [scrollY, setScrollY] = useState(0)const [urgentCount, setUrgentCount] = useState(0)useEffect(() => {const handler = () => {// โ Non-urgent update wrapped in startTransitionstartTransition(() => {setScrollY(window.scrollY)})// Urgent update (not wrapped) - updates immediatelysetUrgentCount(c => c + 1)}window.addEventListener('scroll', handler, { passive: true })return () => window.removeEventListener('scroll', handler)}, [])return (<div style={{ padding: '2rem', minHeight: '200vh' }}><div style={{position: 'fixed',top: '1rem',right: '1rem',background: '#1e293b',padding: '1rem',borderRadius: '8px',border: '1px solid #334155',minWidth: '200px'}}><h3 style={{ color: '#fff', margin: '0 0 0.5rem 0', fontSize: '0.875rem' }}>Scroll Position</h3><div style={{ color: '#94a3b8', fontSize: '0.75rem', marginBottom: '0.5rem' }}>Y: {Math.round(scrollY)}px</div><div style={{ color: '#10b981', fontSize: '0.75rem' }}>Updates: {urgentCount}</div><div style={{marginTop: '0.5rem',padding: '0.5rem',background: '#0f172a',borderRadius: '4px',fontSize: '0.7rem',color: '#64748b'}}>โ Scroll position uses startTransition<br />โ Keeps UI responsive</div></div><div style={{ padding: '2rem', color: '#fff' }}><h2 style={{ marginBottom: '1rem' }}>Scroll Down to See Transitions</h2><p style={{ color: '#94a3b8', lineHeight: '1.6' }}>The scroll position updates are marked as non-urgent using startTransition,which keeps the UI responsive even during rapid scrolling. Try scrollingquickly and notice how the UI remains smooth.</p></div></div>)}export const App = () => <ScrollTracker />
Calculate Derived State During Rendering (MEDIUM)
๐งฎ If you can compute it from props or state, just... compute it! Don't store derived values in state and sync with useEffect โ that's a Rube Goldberg machine for a simple calculation! ๐ช The effect approach causes an extra render cycle (render โ effect โ setState โ re-render) and the derived value is stale for one frame.
What to notice
- useState + useEffect: two render cycles to compute fullName.
- For one frame, fullName is stale (empty string) before the effect runs.
function UserCard({ firstName, lastName }) {const [fullName, setFullName] = useState('')useEffect(() => {setFullName(`${firstName} ${lastName}`)}, [firstName, lastName])return <div>{fullName}</div>}
What to notice
- Derived during render: always in sync, single render cycle.
- No extra state, no effect, no risk of stale values.
function UserCard({ firstName, lastName }) {// โ Derive during render โ always in syncconst fullName = `${firstName} ${lastName}`return <div>{fullName}</div>}// For expensive computations, use useMemo:function FilteredList({ items, query }) {const filtered = useMemo(() => items.filter(i => i.name.includes(query)),[items, query])return <ul>{filtered.map(i => <li key={i.id}>{i.name}</li>)}</ul>}
Hoist Default Non-Primitive Props (MEDIUM)
๐ชค Sneaky bug alert! Default values like () => {}, [], or {} in destructured props create a NEW reference on every render, silently breaking React.memo! ๐ค Your memoized child re-renders every time even though "nothing changed". Extract defaults to module-level constants and the problem vanishes. โจ
What to notice
- Default () => {} creates a new function reference every render.
- React.memo on Child is useless โ onChange is always a new reference.
// โ Default creates new reference every renderfunction Parent({ onChange = () => {} }) {return <MemoizedChild onChange={onChange} />}function Parent2({ items = [] }) {return <MemoizedList items={items} />}
What to notice
- Module-level constant: same reference across all renders.
- React.memo on Child works correctly โ onChange is referentially stable.
// โ Hoist defaults to module scopeconst noop = () => {}const emptyArray: Item[] = []function Parent({ onChange = noop }) {return <MemoizedChild onChange={onChange} />}function Parent2({ items = emptyArray }) {return <MemoizedList items={items} />}
Don't Wrap Simple Expressions in useMemo (LOW-MEDIUM)
๐ค Plot twist: useMemo has overhead! It stores previous values, compares dependencies, and manages cache entries. For a simple user.role === 'admin' check? The memoization costs MORE than just computing it! ๐ Save useMemo for the heavy stuff โ filtering large arrays, complex transformations, expensive calculations.
What to notice
- useMemo for a simple boolean comparison โ overhead exceeds the computation.
- useMemo for basic string concatenation โ completely unnecessary.
function UserStatus({ user }) {// โ useMemo overhead > simple boolean checkconst isAdmin = useMemo(() => user.role === 'admin', [user.role])// โ useMemo overhead > string concatenationconst greeting = useMemo(() => `Hello, ${user.name}!`,[user.name])return <div>{greeting} {isAdmin && '(Admin)'}</div>}
What to notice
- Direct expressions: faster than useMemo's dependency-checking overhead.
- Reserve useMemo for expensive operations like filtering large arrays.
function UserStatus({ user }) {// โ Direct computation โ cheaper than useMemoconst isAdmin = user.role === 'admin'const greeting = `Hello, ${user.name}!`// โ Reserve useMemo for expensive workconst sortedPosts = useMemo(() => user.posts.toSorted((a, b) => b.date - a.date),[user.posts])return <div>{greeting} {isAdmin && '(Admin)'}</div>}
Put Interaction Logic in Event Handlers (MEDIUM)
๐ฏ Stop playing telephone with your side effects! Don't do "user clicks โ setState โ effect detects state โ runs API call". That's an unnecessary game of telephone! ๐ Just run the side effect directly in the event handler. Clearer code, fewer renders, less room for bugs.
What to notice
- State change triggers effect which sends the message โ indirect, hard to follow.
- Extra render cycle: setState โ render โ effect โ API call.
function ChatInput() {const [message, setMessage] = useState('')const [submitted, setSubmitted] = useState(false)// โ Effect watches for submitted stateuseEffect(() => {if (submitted) {sendMessage(message)setMessage('')setSubmitted(false)}}, [submitted, message])return (<form onSubmit={(e) => {e.preventDefault()setSubmitted(true) // Indirect: triggers effect}}><input value={message} onChange={e => setMessage(e.target.value)} /></form>)}
What to notice
- Handler sends the message directly โ clear cause-and-effect.
- Single render, no unnecessary intermediate state.
function ChatInput() {const [message, setMessage] = useState('')// โ Side effect runs directly in the handlerconst handleSubmit = (e) => {e.preventDefault()sendMessage(message) // Direct: cause and effectsetMessage('')}return (<form onSubmit={handleSubmit}><input value={message} onChange={e => setMessage(e.target.value)} /></form>)}
Use useRef for Transient Values (MEDIUM)
๐ญ Tracking mouse position with useState? That's 60+ re-renders PER SECOND! ๐คฏ Values that change fast but don't need to update the UI โ mouse coords, scroll offsets, interval IDs โ belong in useRef. It's like a secret notebook React doesn't watch. Update it as often as you want, zero re-renders! ๐ฅท
What to notice
- useState for mouse position: 60+ re-renders per second during mousemove.
- Every pixel of mouse movement triggers a full component re-render.
function Canvas() {// โ 60+ re-renders/sec from mouse movementconst [mousePos, setMousePos] = useState({ x: 0, y: 0 })useEffect(() => {const handler = (e) => {setMousePos({ x: e.clientX, y: e.clientY })}window.addEventListener('mousemove', handler)return () => window.removeEventListener('mousemove', handler)}, [])const handleClick = () => {placeMarker(mousePos.x, mousePos.y)}return <div onClick={handleClick}>Canvas</div>}
What to notice
- useRef stores the position without triggering re-renders.
- Read mouseRef.current when needed (e.g., on click) โ zero re-renders from tracking.
function Canvas() {// โ Zero re-renders from mouse trackingconst mouseRef = useRef({ x: 0, y: 0 })useEffect(() => {const handler = (e) => {mouseRef.current = { x: e.clientX, y: e.clientY }}window.addEventListener('mousemove', handler)return () => window.removeEventListener('mousemove', handler)}, [])const handleClick = () => {const { x, y } = mouseRef.currentplaceMarker(x, y)}return <div onClick={handleClick}>Canvas</div>}// Also useful for interval/timeout IDsfunction Poller() {const intervalRef = useRef<NodeJS.Timeout>()useEffect(() => {intervalRef.current = setInterval(poll, 5000)return () => clearInterval(intervalRef.current)}, [])const stopPolling = () => clearInterval(intervalRef.current)return <button onClick={stopPolling}>Stop</button>}
๐จ 6. Rendering Performance
๐๏ธ Once React commits changes to the DOM, the browser still has work to do โ layout, paint, composite. Let's make that work as light as possible! ๐๏ธ Animate SVG wrappers for GPU acceleration, use content-visibility to skip off-screen layout, hoist static JSX, trim SVG precision, suppress expected hydration mismatches, use React's Activity component, replace manual loading states with useTransition, and always use explicit ternary conditionals (0 rendering as text is a classic gotcha! ๐ฌ).
๐ต Impact: MEDIUM โ These optimizations make your app feel polished and smooth. Users notice jank even if they can't explain it! ๐
๐ In this section: SVG animation โข content-visibility โข Static JSX hoisting โข Hydration mismatches โข Activity component โข Ternary conditionals โข SVG precision โข suppressHydrationWarning โข useTransition loading
Animate SVG Wrapper, Not SVG Element
๐ฌ Most browsers can't GPU-accelerate CSS animations directly on SVG elements โ they fall back to software rendering! ๐ The fix is dead simple: wrap your SVG in a <div> and animate the wrapper. Same visual result, but now the GPU handles it. Buttery smooth! ๐ง
function LoadingSpinner() {return (<svgclassName="animate-spin"width="24"height="24"viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="currentColor" /></svg>)}
function LoadingSpinner() {return (<div className="animate-spin"><svgwidth="24"height="24"viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="currentColor" /></svg></div>)}
๐ฏ This applies to all CSS transforms โ transform, opacity, translate, scale, rotate. The wrapper div unlocks GPU acceleration for silky smooth animations! ๐ง
import { useState } from 'react'// โ Wrong: Animating SVG directly (no hardware acceleration)function BadSpinner() {return (<svgstyle={{animation: 'spin 1s linear infinite',width: '48px',height: '48px',viewBox: '0 0 24 24'}}><style>{'@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }'}</style><circle cx="12" cy="12" r="10" stroke="#ef4444" strokeWidth="2" fill="none" /></svg>)}// โ Correct: Animating wrapper div (hardware accelerated)function GoodSpinner() {return (<div style={{animation: 'spin 1s linear infinite',width: '48px',height: '48px',display: 'flex',alignItems: 'center',justifyContent: 'center'}}><style>{'@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }'}</style><svg width="48" height="48" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="#10b981" strokeWidth="2" fill="none" /></svg></div>)}export const App = () => {return (<div style={{ padding: '2rem', textAlign: 'center' }}><h2 style={{ color: '#fff', marginBottom: '2rem' }}>SVG Animation Comparison</h2><div style={{ display: 'flex', gap: '3rem', justifyContent: 'center', alignItems: 'center' }}><div><div style={{ color: '#fca5a5', marginBottom: '0.5rem', fontSize: '0.875rem' }}>โ Wrong (SVG element)</div><BadSpinner /><div style={{ color: '#94a3b8', marginTop: '0.5rem', fontSize: '0.75rem' }}>No GPU acceleration</div></div><div><div style={{ color: '#86efac', marginBottom: '0.5rem', fontSize: '0.875rem' }}>โ Correct (Wrapper div)</div><GoodSpinner /><div style={{ color: '#94a3b8', marginTop: '0.5rem', fontSize: '0.75rem' }}>GPU accelerated</div></div></div><p style={{ color: '#94a3b8', marginTop: '2rem', fontSize: '0.875rem' }}>The wrapper div approach enables hardware acceleration for smoother animations.</p></div>)}
CSS content-visibility for Long Lists
๐ Got a list with 1000+ items? The browser is doing layout and paint for ALL of them, even the 990 you can't see! ๐ Apply content-visibility: auto and the browser skips off-screen items entirely. That's a potential 10ร faster initial render! ๐
/* CSS:.message-item {content-visibility: auto;contain-intrinsic-size: 0 80px;}*/function MessageList({ messages }) {return (<div style={{ overflowY: 'auto', height: '100vh' }}>{messages.map(msg => (<divkey={msg.id}className="message-item"style={{padding: '1rem',borderBottom: '1px solid rgba(255,255,255,0.1)'}}><div style={{ fontWeight: 'bold', marginBottom: '0.5rem' }}>{msg.author}</div><div>{msg.content}</div></div>))}</div>)}// Example usageexport const App = () => {const messages = Array.from({ length: 100 }, (_, i) => ({id: i,author: `User ${i + 1}`,content: `Message content ${i + 1}`}))return (<div style={{ padding: '2rem' }}><h2 style={{ color: '#fff', marginBottom: '1rem' }}>Long List with content-visibility</h2><p style={{ color: '#94a3b8', marginBottom: '1rem' }}>Scroll to see how off-screen items are deferred</p><MessageList messages={messages} /></div>)}
Hoist Static JSX Elements
๐๏ธ If your JSX never changes, why recreate it every render? Extract static elements outside the component โ React will reuse the same object reference. Especially impactful for big SVG nodes! ๐จ
function LoadingSkeleton() {return <div className="animate-pulse h-20 bg-gray-200" />}function Container() {return (<div>{loading && <LoadingSkeleton />}</div>)}
const loadingSkeleton = (<div className="animate-pulse h-20 bg-gray-200" />)function Container() {return (<div>{loading && loadingSkeleton}</div>)}
๐ค Note: If React Compiler is enabled in your project, it does this automatically! One less thing to worry about. โจ
Prevent Hydration Mismatch Without Flickering
function ThemeWrapper({ children }) {const [theme, setTheme] = useState('light')useEffect(() => {const stored = localStorage.getItem('theme')if (stored) setTheme(stored)}, [])return <div className={theme}>{children}</div>}
function ThemeWrapper({ children }) {return (<><div id="theme-wrapper">{children}</div><script dangerouslySetInnerHTML={{ __html: `(function() {try {var theme = localStorage.getItem('theme') || 'light';var el = document.getElementById('theme-wrapper');if (el) el.className = theme;} catch (e) {}})();` }} /></>)}
Use Activity for Show/Hide (React 19)
import { Activity } from 'react'function Dropdown({ isOpen }) {return (<Activity mode={isOpen ? 'visible' : 'hidden'}><ExpensiveMenu /></Activity>)}
Use Explicit Conditional (Ternary, Not &&)
function Badge({ count }) {return <div>{count && <span className="badge">{count}</span>}</div>}
function Badge({ count }) {return <div>{count > 0 ? <span className="badge">{count}</span> : null}</div>}
Interactive Example: Conditional Rendering
import { useState } from 'react'// โ Wrong: Renders "0" when count is 0function BadBadge({ count }) {return (<div style={{ padding: '1rem', background: '#7f1d1d', borderRadius: '8px', marginBottom: '1rem' }}><div style={{ color: '#fca5a5', marginBottom: '0.5rem' }}>โ Wrong Approach</div><div style={{ color: '#fff' }}>Count: {count && <span style={{background: '#ef4444',padding: '0.25rem 0.5rem',borderRadius: '4px',marginLeft: '0.5rem'}}>{count}</span>}</div>{count === 0 && (<div style={{ color: '#fca5a5', fontSize: '0.75rem', marginTop: '0.5rem' }}>โ ๏ธ Renders "0" instead of nothing!</div>)}</div>)}// โ Correct: Explicit ternaryfunction GoodBadge({ count }) {return (<div style={{ padding: '1rem', background: '#14532d', borderRadius: '8px' }}><div style={{ color: '#86efac', marginBottom: '0.5rem' }}>โ Correct Approach</div><div style={{ color: '#fff' }}>Count: {count > 0 ? (<span style={{background: '#10b981',padding: '0.25rem 0.5rem',borderRadius: '4px',marginLeft: '0.5rem'}}>{count}</span>) : null}</div>{count === 0 && (<div style={{ color: '#86efac', fontSize: '0.75rem', marginTop: '0.5rem' }}>โ Renders nothing when count is 0</div>)}</div>)}export const App = () => {const [count, setCount] = useState(0)return (<div style={{ padding: '2rem', maxWidth: '500px', margin: '0 auto' }}><h2 style={{ color: '#fff', marginBottom: '1rem' }}>Conditional Rendering Demo</h2><div style={{ marginBottom: '1.5rem', display: 'flex', gap: '0.5rem', alignItems: 'center' }}><buttononClick={() => setCount(c => Math.max(0, c - 1))}style={{padding: '0.5rem 1rem',background: '#ef4444',color: '#fff',border: 'none',borderRadius: '6px',cursor: 'pointer'}}>-</button><span style={{ color: '#fff', minWidth: '60px', textAlign: 'center' }}>Count: {count}</span><buttononClick={() => setCount(c => c + 1)}style={{padding: '0.5rem 1rem',background: '#10b981',color: '#fff',border: 'none',borderRadius: '6px',cursor: 'pointer'}}>+</button></div><BadBadge count={count} /><GoodBadge count={count} /><div style={{marginTop: '1.5rem',padding: '1rem',background: '#1e293b',borderRadius: '8px',fontSize: '0.875rem',color: '#94a3b8'}}><strong style={{ color: '#fff' }}>Try setting count to 0:</strong><br />โข Wrong approach renders "0" as text<br />โข Correct approach renders nothing</div></div>)}
Optimize SVG Precision
โ๏ธ Does your SVG really need 6 decimal places? M 10.293847 20.847362 โ M 10.3 20.8 โ same visual result, 40% smaller file! ๐ Use SVGO to automate this across all your icons.
<!-- Excessive precision: 15 bytes --><path d="M 10.293847 20.847362 L 30.938472 40.192837" />
<!-- Optimized precision: 9 bytes (40% smaller) --><path d="M 10.3 20.8 L 30.9 40.2" /><!-- Automate with SVGO --><!-- npx svgo --precision=1 --multipass icon.svg -->
// โ SVG Optimization Example// Before optimization (excessive precision)const beforeOptimization = `<svg viewBox="0 0 100 100"><path d="M 10.293847 20.847362 L 30.938472 40.192837 L 50.123456 60.789012" /></svg>`// After optimization (1 decimal place)const afterOptimization = `<svg viewBox="0 0 100 100"><path d="M 10.3 20.8 L 30.9 40.2 L 50.1 60.8" /></svg>`console.log('Before:', beforeOptimization.length, 'bytes')console.log('After:', afterOptimization.length, 'bytes')console.log('Savings:', Math.round((1 - afterOptimization.length / beforeOptimization.length) * 100) + '%')// Use SVGO CLI for batch optimization:// npx svgo --precision=1 --multipass --folder=./icons --output=./icons-optimizedexport const App = () => {return (<div style={{ padding: '2rem', color: '#fff' }}><h2 style={{ marginBottom: '1rem' }}>SVG Precision Optimization</h2><div style={{padding: '1rem',background: '#1e293b',borderRadius: '8px',fontSize: '0.875rem',fontFamily: 'monospace'}}><div style={{ marginBottom: '0.5rem' }}><strong>Before:</strong> {beforeOptimization.length} bytes</div><div style={{ marginBottom: '0.5rem' }}><strong>After:</strong> {afterOptimization.length} bytes</div><div style={{ color: '#10b981' }}><strong>Savings:</strong> {Math.round((1 - afterOptimization.length / beforeOptimization.length) * 100)}%</div></div><p style={{ color: '#94a3b8', marginTop: '1rem', fontSize: '0.875rem' }}>Check console for details. Use SVGO CLI for batch optimization.</p></div>)}
Suppress Expected Hydration Mismatches (LOW-MEDIUM)
โฐ Some values are just inherently different between server and client โ timestamps, random IDs, locale dates. Instead of the classic "useEffect + useState" dance to fix the hydration warning (which causes a flash! ๐ซ), just use suppressHydrationWarning. Tell React "I know this is different, it's fine!" ๐ One render, no flash, no extra state.
What to notice
- useEffect + useState causes a flash: first render shows nothing, then the date appears.
- Two renders on the client just to display a timestamp.
function LastUpdated() {const [date, setDate] = useState('')useEffect(() => {// โ Extra render to avoid hydration warningsetDate(new Date().toLocaleString())}, [])return <span>{date}</span>}
What to notice
- suppressHydrationWarning tells React this mismatch is expected.
- Single render, no flash, no extra state โ server value is replaced seamlessly.
function LastUpdated() {return (<span suppressHydrationWarning>{new Date().toLocaleString()}</span>)}// Also useful for random IDsfunction UniqueField() {return (<inputid={crypto.randomUUID()}suppressHydrationWarning/>)}
Use useTransition Over Manual Loading States (LOW)
๐ Stop managing isLoading state manually โ React has a better way! useTransition gives you a free isPending boolean, keeps your current UI visible while processing (no jarring spinners! ๐ก), and plays perfectly with Suspense. Less code, better UX! ๐
What to notice
- Manual isLoading state requires three setState calls (loading start, data, loading end).
- UI blanks out immediately โ user sees a spinner while waiting.
function FilteredList({ items }) {const [filter, setFilter] = useState('')const [filtered, setFiltered] = useState(items)const [isLoading, setIsLoading] = useState(false)const handleFilter = (value) => {setIsLoading(true)setFilter(value)// Simulate expensive filterconst result = items.filter(i => i.name.includes(value))setFiltered(result)setIsLoading(false)}return (<div><input onChange={e => handleFilter(e.target.value)} />{isLoading ? <Spinner /> : <List items={filtered} />}</div>)}
What to notice
- useTransition gives you isPending automatically โ no manual loading state.
- Previous UI stays visible (non-blocking) until the new data is ready.
import { useState, useTransition } from 'react'function FilteredList({ items }) {const [filter, setFilter] = useState('')const [filtered, setFiltered] = useState(items)const [isPending, startTransition] = useTransition()const handleFilter = (value) => {setFilter(value)startTransition(() => {// Non-urgent: previous list stays visiblesetFiltered(items.filter(i => i.name.includes(value)))})}return (<div><input onChange={e => handleFilter(e.target.value)} /><div style={{ opacity: isPending ? 0.6 : 1 }}><List items={filtered} /></div></div>)}
๐ 7. JavaScript Performance
๐ฌ Micro-optimizations that individually seem tiny but compound massively in hot paths โ tight loops over thousands of items, real-time data crunching, or 60fps animation frames. ๐ช Batch DOM reads/writes (layout thrashing is brutal!), swap Array.includes() for Set.has() (O(n) โ O(1)!), cache property access in loops, combine array iterations into single passes, and always prefer toSorted() over sort() for immutability. These are the tricks that separate good code from blazing fast code! ๐ฅ
๐ฃ Impact: LOW-MEDIUM โ Each trick saves microseconds, but in hot loops processing 10K+ items, they add up to milliseconds that matter! โก
๐ In this section: DOM batching โข Set/Map lookups โข Index maps โข Property caching โข Function caching โข Storage caching โข Combined iterations โข Length checks โข Early returns โข RegExp hoisting โข Loop min/max โข toSorted()
Batch DOM CSS Changes
element.style.width = '100px'const w = element.offsetWidthelement.style.height = '200px'
element.style.width = '100px'element.style.height = '200px'const { width, height } = element.getBoundingClientRect()
Use Set/Map for O(1) Lookups
const allowedIds = ['a', 'b', 'c']items.filter(item => allowedIds.includes(item.id))
const allowedIds = new Set(['a', 'b', 'c'])items.filter(item => allowedIds.has(item.id))
Build Index Maps for Repeated Lookups
return orders.map(order => ({...order,user: users.find(u => u.id === order.userId)}))
const userById = new Map(users.map(u => [u.id, u]))return orders.map(order => ({...order,user: userById.get(order.userId)}))
Cache Property Access in Loops
for (let i = 0; i < arr.length; i++) {process(obj.config.settings.value)}
const value = obj.config.settings.valueconst len = arr.lengthfor (let i = 0; i < len; i++) process(value)
Cache Repeated Function Calls
projects.map(p => {const slug = slugify(p.name)return <ProjectCard key={p.id} slug={slug} />})
const slugifyCache = new Map()function cachedSlugify(text) {if (slugifyCache.has(text)) return slugifyCache.get(text)const r = slugify(text)slugifyCache.set(text, r)return r}projects.map(p => {const slug = cachedSlugify(p.name)return <ProjectCard key={p.id} slug={slug} />})
Cache Storage API Calls
const storageCache = new Map()function getLocalStorage(key) {if (!storageCache.has(key))storageCache.set(key, localStorage.getItem(key))return storageCache.get(key)}function setLocalStorage(key, value) {localStorage.setItem(key, value)storageCache.set(key, value)}
Combine Multiple Array Iterations
const admins = users.filter(u => u.isAdmin)const testers = users.filter(u => u.isTester)const inactive = users.filter(u => !u.isActive)
const admins = [], testers = [], inactive = []for (const user of users) {if (user.isAdmin) admins.push(user)if (user.isTester) testers.push(user)if (!user.isActive) inactive.push(user)}
Early Length Check for Array Comparisons
function hasChanges(current, original) {return current.sort().join() !== original.sort().join()}
function hasChanges(current, original) {if (current.length !== original.length) return trueconst a = current.toSorted(), b = original.toSorted()for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return truereturn false}
Early Return from Functions
function validateUsers(users) {let hasError = false, errorMessage = ''for (const u of users) {if (!u.email) { hasError = true; errorMessage = 'Email required' }if (!u.name) { hasError = true; errorMessage = 'Name required' }}return hasError ? { valid: false, error: errorMessage } : { valid: true }}
function validateUsers(users) {for (const u of users) {if (!u.email) return { valid: false, error: 'Email required' }if (!u.name) return { valid: false, error: 'Name required' }}return { valid: true }}
Hoist RegExp Creation
function Highlighter({ text, query }) {const regex = new RegExp(`(${query})`, 'gi')const parts = text.split(regex)return <>{parts.map((p, i) => <span key={i}>{p}</span>)}</>}
function Highlighter({ text, query }) {const regex = useMemo(() => new RegExp(`(${escapeRegex(query)})`, 'gi'),[query])const parts = text.split(regex)return <>{parts.map((p, i) => <span key={i}>{p}</span>)}</>}
Use Loop for Min/Max Instead of Sort
function getLatestProject(projects) {const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)return sorted[0]}
function getLatestProject(projects) {if (projects.length === 0) return nulllet latest = projects[0]for (let i = 1; i < projects.length; i++) {if (projects[i].updatedAt > latest.updatedAt) latest = projects[i]}return latest}
Use toSorted() Instead of sort()
const sorted = useMemo(() => users.sort((a, b) => a.name.localeCompare(b.name)),[users])
const sorted = useMemo(() => users.toSorted((a, b) => a.name.localeCompare(b.name)),[users])
๐งฉ 8. Advanced Patterns
๐งโโ๏ธ These are the patterns that separate React apprentices from React wizards! They solve subtle, maddening bugs that only appear in complex apps. Store event handlers in refs for rock-solid subscriptions that don't re-attach on every render, use useLatest for stable callback refs (goodbye dependency array headaches! ๐), and initialize app-wide services at module level โ because useEffect running twice in Strict Mode is not the init behavior you want! ๐
๐ข Impact: LOW โ Niche but powerful. When you need these patterns, you'll be glad you know them! ๐ฏ
๐ In this section: Event handler refs โข useLatest pattern โข Module-level initialization
Store Event Handlers in Refs
function useWindowEvent(event, handler) {useEffect(() => {window.addEventListener(event, handler)return () => window.removeEventListener(event, handler)}, [event, handler])}
function useWindowEvent(event, handler) {const handlerRef = useRef(handler)useEffect(() => { handlerRef.current = handler }, [handler])useEffect(() => {const listener = (e) => handlerRef.current(e)window.addEventListener(event, listener)return () => window.removeEventListener(event, listener)}, [event])}
useLatest for Stable Callback Refs
function SearchInput({ onSearch }) {const [query, setQuery] = useState('')useEffect(() => {const t = setTimeout(() => onSearch(query), 300)return () => clearTimeout(t)}, [query, onSearch])}
function useLatest(value) {const ref = useRef(value)useLayoutEffect(() => { ref.current = value }, [value])return ref}function SearchInput({ onSearch }) {const [query, setQuery] = useState('')const onSearchRef = useLatest(onSearch)useEffect(() => {const t = setTimeout(() => onSearchRef.current(query), 300)return () => clearTimeout(t)}, [query])}
Initialize App Once, Not Per Mount (LOW-MEDIUM)
๐ Analytics, feature flags, SDK setup โ these should run ONCE, period. But useEffect runs on every mount (and twice in Strict Mode! ๐ตโ๐ซ). Put init logic at module level with a simple boolean guard. Runs before the first render, never again. Clean and predictable! โ
What to notice
- useEffect runs on every mount (twice in Strict Mode).
- Analytics or SDK init runs multiple times, causing duplicate events or errors.
function App() {useEffect(() => {// โ Runs on every mount (twice in Strict Mode)analytics.init('key-123')featureFlags.load()}, [])return <Main />}
What to notice
- Module-level flag guarantees init runs exactly once.
- No dependency on component lifecycle โ runs before first render.
// โ Module-level guard: runs exactly oncelet initialized = falsefunction initApp() {if (initialized) returninitialized = trueanalytics.init('key-123')featureFlags.load()}function App() {initApp() // Safe to call multiple timesreturn <Main />}