React Performance
by @wpank
React and Next.js performance optimization patterns. Use when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance. Triggers on tasks involving components, data fetching, bundle optimization, re-render reduction, or server component architecture.
clawhub install react-performanceπ About This Skill
name: react-performance model: standard description: React and Next.js performance optimization patterns. Use when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance. Triggers on tasks involving components, data fetching, bundle optimization, re-render reduction, or server component architecture. version: "1.0"
React Performance Patterns
Performance optimization guide for React and Next.js applications. Patterns
across 7 categories, prioritized by impact. Detailed examples in references/.
When to Apply
Categories by Priority
| # | Category | Impact | |---|----------------------|------------| | 1 | Async / Waterfalls | CRITICAL | | 2 | Bundle Size | CRITICAL | | 3 | Server Components | HIGH | | 4 | Re-renders | MEDIUM | | 5 | Rendering | MEDIUM | | 6 | Client-Side Data | MEDIUM | | 7 | JS Performance | LOW-MEDIUM |
Installation
OpenClaw / Moltbot / Clawbot
npx clawhub@latest install react-performance
1. Async β Eliminating Waterfalls (CRITICAL)
Parallelize independent operations
Sequential awaits are the single biggest performance mistake in React apps.
// BAD β sequential, 3 round trips
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()// GOOD β parallel, 1 round trip
const [user, posts, comments] = await Promise.all([
fetchUser(), fetchPosts(), fetchComments(),
])
Defer await until needed
Move await into branches where the value is actually used.
// BAD β blocks both branches
async function handle(userId: string, skip: boolean) {
const data = await fetchUserData(userId)
if (skip) return { skipped: true } // Still waited
return process(data)
}// GOOD β only blocks when needed
async function handle(userId: string, skip: boolean) {
if (skip) return { skipped: true } // Returns immediately
return process(await fetchUserData(userId))
}
Strategic Suspense boundaries
Show layout immediately while data-dependent sections load independently.
// BAD β entire page blocked
async function Page() {
const data = await fetchData()
return
}// GOOD β layout renders immediately, data streams in
function Page() {
return (
}>
)
}
async function DataDisplay() {
const data = await fetchData()
return {data.content}
}
Share a promise across components with use() to avoid duplicate fetches.
2. Bundle Size (CRITICAL)
Avoid barrel file imports
Barrel files load thousands of unused modules. Direct imports save 200-800ms.
// BAD β loads 1,583 modules
import { Check, X, Menu } from 'lucide-react'// GOOD β loads only 3 modules
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'
Next.js 13.5+: use experimental.optimizePackageImports in config.
Commonly affected: lucide-react, @mui/material, react-icons, @radix-ui,
lodash, date-fns.
Dynamic imports for heavy components
import dynamic from 'next/dynamic'
const MonacoEditor = dynamic(
() => import('./monaco-editor').then((m) => m.MonacoEditor),
{ ssr: false }
)
Defer non-critical third-party libraries
Analytics, logging, error tracking β load after hydration with dynamic() and
{ ssr: false }.
Preload on user intent
const preload = () => { void import('./monaco-editor') }
3. Server Components (HIGH)
Minimize serialization at RSC boundaries
Only pass fields the client actually uses across the server/client boundary.
// BAD β serializes all 50 user fields
return // GOOD β serializes 1 field
return
Parallel data fetching with composition
RSC execute sequentially within a tree. Restructure to parallelize.
// BAD β Sidebar waits for header fetch
export default async function Page() {
const header = await fetchHeader()
return {header}
}// GOOD β sibling async components fetch simultaneously
async function Header() { return
{await fetchHeader()} }
async function Sidebar() { return }
export default function Page() { return }
React.cache() for per-request deduplication
import { cache } from 'react'
export const getCurrentUser = cache(async () => {
const session = await auth()
if (!session?.user?.id) return null
return await db.user.findUnique({ where: { id: session.user.id } })
})
Use primitive args (not inline objects) β React.cache() uses Object.is.
Next.js auto-deduplicates fetch, but React.cache() is needed for DB queries,
auth checks, and computations.
after() for non-blocking operations
import { after } from 'next/server'
export async function POST(request: Request) {
await updateDatabase(request)
after(async () => { logUserAction({ userAgent: request.headers.get('user-agent') }) })
return Response.json({ status: 'success' })
}
4. Re-render Optimization (MEDIUM)
Derive state during render β not in effects
// BAD β redundant state + effect
const [fullName, setFullName] = useState('')
useEffect(() => { setFullName(first + ' ' + last) }, [first, last])// GOOD β derive inline
const fullName = first + ' ' + last
Functional setState for stable callbacks
// BAD β recreated on every items change
const addItem = useCallback((item: Item) => {
setItems([...items, item])
}, [items])// GOOD β stable, always latest state
const addItem = useCallback((item: Item) => {
setItems((curr) => [...curr, item])
}, [])
Defer state reads to usage point
Don't subscribe to dynamic state if you only read it in callbacks.
// BAD β re-renders on every searchParams change
const searchParams = useSearchParams()
const handleShare = () => shareChat(chatId, { ref: searchParams.get('ref') })// GOOD β reads on demand
const handleShare = () => {
const ref = new URLSearchParams(window.location.search).get('ref')
shareChat(chatId, { ref })
}
Lazy state initialization
// BAD β JSON.parse runs every render
const [settings] = useState(JSON.parse(localStorage.getItem('s') || '{}'))// GOOD β runs only once
const [settings] = useState(() => JSON.parse(localStorage.getItem('s') || '{}'))
Subscribe to derived booleans
// BAD β re-renders on every pixel
const width = useWindowWidth(); const isMobile = width < 768// GOOD β re-renders only when boolean flips
const isMobile = useMediaQuery('(max-width: 767px)')
Transitions for non-urgent updates
// BAD β blocks UI on scroll
const handler = () => setScrollY(window.scrollY)// GOOD β non-blocking
const handler = () => startTransition(() => setScrollY(window.scrollY))
Extract expensive work into memoized components
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
const id = useMemo(() => computeAvatarId(user), [user])
return
})
function Profile({ user, loading }: Props) {
if (loading) return
return
}
Note: React Compiler makes manual memo()/useMemo() unnecessary.
5. Rendering Performance (MEDIUM)
CSS content-visibility for long lists
For 1000 items, browser skips ~990 off-screen (10x faster initial render).
.list-item { content-visibility: auto; contain-intrinsic-size: 0 80px; }
Hoist static JSX outside components
Avoids re-creation, especially for large SVG nodes. React Compiler does this automatically.
const skeleton =
function Container() { return {loading && skeleton} }
6. Client-Side Data (MEDIUM)
SWR for deduplication and caching
// BAD β each instance fetches independently
useEffect(() => { fetch('/api/users').then(r => r.json()).then(setUsers) }, [])// GOOD β multiple instances share one request
const { data: users } = useSWR('/api/users', fetcher)
7. JS Performance (LOW-MEDIUM)
Set/Map for O(1) lookups
// BAD β O(n)
items.filter(i => allowed.includes(i.id))
// GOOD β O(1)
const allowedSet = new Set(allowed)
items.filter(i => allowedSet.has(i.id))
Combine array iterations
// BAD β 3 passes
const a = users.filter(u => u.isAdmin)
const t = users.filter(u => u.isTester)
// GOOD β 1 pass
const a: User[] = [], t: User[] = []
for (const u of users) { if (u.isAdmin) a.push(u); if (u.isTester) t.push(u) }
Also: early returns, cache property access in loops, hoist RegExp outside
loops, prefer for...of for hot paths.
Quick Decision Guide
1. Slow page load? β Bundle size (2), then async waterfalls (1) 2. Sluggish interactions? β Re-renders (4), then JS perf (7) 3. Server page slow? β RSC serialization & parallel fetching (3) 4. Client data stale/slow? β SWR (6) 5. Long lists janky? β content-visibility (5)