
React Impl Performance
- 12 installs
- 6 repo stars
- Updated July 8, 2026
- openaec-foundation/react-claude-skill-package
Helps with frontend development tasks.
About
react-impl-performance is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted development.
- react-impl-performance
- Frontend Development
- AI-coding skill
React Impl Performance by the numbers
- 12 all-time installs (skills.sh)
- Ranked #1,643 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/react-claude-skill-package --skill react-impl-performanceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 12 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/react-claude-skill-package ↗ |
What it does
Helps with frontend development tasks.
Files
react-impl-performance
Quick Reference
Performance Optimization Tools
| Tool | Purpose | React Version | When to Use |
|---|---|---|---|
React.memo | Skip re-renders when props unchanged | 18 + 19 | Expensive component, same props frequently |
useMemo | Cache expensive computation results | 18 + 19 | Calculation >1ms on target hardware |
useCallback | Stable function reference for children | 18 + 19 | Function prop to memo-wrapped child |
| React Compiler | Automatic memoization at build time | 19+ | Replaces manual memo/useMemo/useCallback |
<Profiler> | Measure render durations | 18 + 19 | Identify slow components |
| React DevTools | Visual profiling (flamegraph, ranked) | 18 + 19 | Interactive performance investigation |
React.lazy | Code splitting per route/component | 18 + 19 | Reduce initial bundle size |
@tanstack/virtual | Virtualize long lists | 18 + 19 | Lists with 1000+ items |
Critical Warnings
NEVER optimize before measuring. ALWAYS use the Profiler component or React DevTools to identify the actual bottleneck first. Premature optimization adds complexity without measurable benefit.
NEVER wrap every component in React.memo. The shallow comparison itself has a cost. ONLY use memo when a component re-renders frequently with the same props AND rendering is noticeably slow.
NEVER use JSON.stringify in a custom arePropsEqual function for React.memo. This is slower than just re-rendering the component.
NEVER rely on useMemo as a semantic guarantee. React MAY discard cached values (on suspend, during development). Use useRef if you need a persistent reference.
---
Decision Tree: When to Optimize
Component renders slowly?
├── NO → STOP. Do not optimize.
└── YES → Measure with Profiler/DevTools
├── Re-renders with same props?
│ ├── Props are primitives → Use React.memo
│ ├── Props include objects → useMemo the object, then React.memo
│ └── Props include functions → useCallback the function, then React.memo
├── Expensive computation during render?
│ └── Use useMemo with dependency array
├── Large bundle size / slow initial load?
│ ├── Route-based → React.lazy + Suspense
│ └── Feature-based → Dynamic import + React.lazy
├── Long list (1000+ items)?
│ └── Use @tanstack/virtual
└── Using React 19?
└── Enable React Compiler → removes need for manual memo/useMemo/useCallback---
React.memo
Wraps a component to skip re-rendering when props have not changed (shallow Object.is comparison per prop).
import { memo } from 'react';
interface ExpensiveListProps {
items: readonly string[];
onSelect: (item: string) => void;
}
const ExpensiveList = memo<ExpensiveListProps>(function ExpensiveList({
items,
onSelect,
}) {
return (
<ul>
{items.map((item) => (
<li key={item} onClick={() => onSelect(item)}>{item}</li>
))}
</ul>
);
});ALWAYS ensure props passed to a memo component are referentially stable. Passing a new object or function literal on every render defeats memo entirely.
memo does NOT prevent re-renders caused by:
- Internal state changes (
useState,useReducer) - Context value changes (
useContext)
---
useMemo
Caches the result of an expensive calculation between re-renders.
import { useMemo } from 'react';
function FilteredList({ items, query }: { items: Item[]; query: string }) {
const filtered = useMemo(
() => items.filter((item) => item.name.includes(query)),
[items, query]
);
return <ul>{filtered.map((item) => <li key={item.id}>{item.name}</li>)}</ul>;
}ALWAYS include every reactive value used inside the calculation in the dependency array. NEVER omit the dependency array — this recalculates every render, defeating the purpose.
---
useCallback
Returns a stable function reference between re-renders. Equivalent to useMemo(() => fn, deps).
import { useCallback } from 'react';
function Parent({ items }: { items: Item[] }) {
const handleSelect = useCallback((id: string) => {
console.log('Selected:', id);
}, []);
return <MemoizedChild items={items} onSelect={handleSelect} />;
}ALWAYS use updater functions to remove state from the dependency array:
// WRONG: todos changes every update, useCallback is useless
const handleAdd = useCallback((text: string) => {
setTodos([...todos, { id: nextId++, text }]);
}, [todos]);
// CORRECT: updater removes todos dependency
const handleAdd = useCallback((text: string) => {
setTodos((prev) => [...prev, { id: nextId++, text }]);
}, []);---
React Compiler (React 19+)
The React Compiler automatically applies memoization at build time, replacing manual memo, useMemo, and useCallback. When enabled, you do NOT need to write these manually.
Setup (Vite)
npm install -D babel-plugin-react-compiler@latest// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
babel: {
plugins: ['babel-plugin-react-compiler'],
},
}),
],
});The babel plugin MUST run first in the pipeline. See references/patterns.md for Next.js and Webpack setup.
Opt-Out Directive
function ProblematicComponent() {
"use no memo";
// Compiler skips this component
return <div>Not compiled</div>;
}Verification
In React DevTools, compiled components show a "Memo" badge with a sparkle icon.
ALWAYS install eslint-plugin-react-hooks@latest — it identifies Rules of React violations that prevent the compiler from optimizing a component.
---
Profiler Component
Measures render performance programmatically. ALWAYS use this to identify bottlenecks before optimizing.
import { Profiler } from 'react';
function onRender(
id: string,
phase: 'mount' | 'update' | 'nested-update',
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number,
): void {
console.log(`${id} [${phase}]: ${actualDuration.toFixed(2)}ms (base: ${baseDuration.toFixed(2)}ms)`);
}
function App() {
return (
<Profiler id="Dashboard" onRender={onRender}>
<Dashboard />
</Profiler>
);
}| Callback Parameter | Meaning |
|---|---|
actualDuration | Time spent rendering this commit (memoization benefit visible here) |
baseDuration | Time to render without any memoization (worst case) |
phase | 'mount' = first render, 'update' = re-render |
Compare actualDuration vs baseDuration to measure memoization effectiveness.
Caveats: Disabled in production builds by default. Use a profiling build for production measurement.
---
React DevTools Profiler
Use the browser extension Profiler tab for interactive investigation:
1. Flamegraph — visual tree of render durations. Gray = did not render (memoized) 2. Ranked chart — components sorted by render time (longest first) 3. "Why did this render?" — enable in Profiler settings to see the exact cause
ALWAYS check "Why did this render?" before adding memoization. Common causes:
- Parent re-rendered (fix with
memo) - Props changed (check referential equality)
- Context changed (split contexts or use selectors)
- Hook state changed (expected behavior)
---
Code Splitting with React.lazy
Split code by route or feature to reduce initial bundle size.
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}ALWAYS use route-based splitting as the primary strategy — it provides the biggest impact with the least effort.
NEVER place lazy() calls inside a component. ALWAYS declare them at module level to prevent recreating the lazy component on every render.
---
Bundle Analysis
Use rollup-plugin-visualizer (Vite) to identify large chunks:
npm install -D rollup-plugin-visualizer// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
react(),
visualizer({ open: true, filename: 'stats.html' }),
],
});Run npm run build and inspect stats.html to find oversized dependencies.
---
Virtualization for Long Lists
NEVER render 1000+ DOM nodes. Use @tanstack/react-virtual to render only visible items.
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
function VirtualList({ items }: { items: string[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 35,
});
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
{items[virtualRow.index]}
</div>
))}
</div>
</div>
);
}---
Reference Links
- references/examples.md — Complete optimization examples with before/after code
- references/patterns.md — Performance patterns: React Compiler setup, context optimization, lazy loading strategies
- references/anti-patterns.md — Common performance mistakes and premature optimization traps
Official Sources
- https://react.dev/reference/react/memo
- https://react.dev/reference/react/useMemo
- https://react.dev/reference/react/useCallback
- https://react.dev/reference/react/Profiler
- https://react.dev/reference/react/lazy
- https://react.dev/learn/react-compiler
react-impl-performance — Anti-Patterns
Anti-Pattern 1: Premature Optimization
// WRONG: Wrapping everything in memo/useMemo/useCallback "just in case"
const MemoizedHeader = memo(function Header() {
return <h1>Welcome</h1>;
});
const title = useMemo(() => 'Welcome', []);
const handleClick = useCallback(() => alert('hi'), []);WHY this is wrong: memo, useMemo, and useCallback add overhead (comparison cost, memory for cached values). For cheap components, the comparison cost exceeds the re-render cost. ALWAYS measure with Profiler first.
CORRECT approach: Write code without optimization. Profile with React DevTools. Optimize ONLY the components that show measurable slowness.
---
Anti-Pattern 2: useMemo for Trivial Calculations
// WRONG: String concatenation is trivial — useMemo adds overhead for no benefit
const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);
// CORRECT: Calculate inline during render
const fullName = `${firstName} ${lastName}`;Rule of thumb: If the calculation takes less than 1ms on your slowest target device, do NOT memoize it.
---
Anti-Pattern 3: useCallback Without memo on the Child
// WRONG: useCallback is pointless — Child is NOT memoized
function Parent() {
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <Child onClick={handleClick} />;
}
// Child re-renders anyway because it is not wrapped in memo
function Child({ onClick }: { onClick: () => void }) {
return <button onClick={onClick}>Click</button>;
}WHY this is wrong: useCallback only prevents re-renders when paired with React.memo on the receiving component. Without memo, the child re-renders whenever the parent re-renders regardless of prop stability.
CORRECT: Either add memo to the child, or remove useCallback from the parent.
---
Anti-Pattern 4: JSON.stringify in Custom arePropsEqual
// WRONG: JSON.stringify is slower than just re-rendering
const MemoizedComponent = memo(MyComponent, (prev, next) => {
return JSON.stringify(prev) === JSON.stringify(next);
});WHY this is wrong: JSON.stringify traverses the entire prop tree, converting to strings, then comparing strings. This is almost always slower than letting React do a shallow comparison plus a re-render.
CORRECT: Use the default shallow comparison. If specific deep props need comparison, compare only those specific values.
---
Anti-Pattern 5: Ignoring Function Props in Custom Comparisons
// WRONG: Skipping function comparison causes stale closures
const MemoizedChild = memo(Child, (prev, next) => {
return prev.data === next.data;
// MISSING: prev.onSelect === next.onSelect
});WHY this is wrong: If onSelect captures state via closure, the child will keep calling the old version with stale state. This leads to bugs where the handler uses outdated values.
CORRECT: Compare ALL props, or use the default comparison (which already does this).
---
Anti-Pattern 6: Creating Objects/Arrays in JSX Props
// WRONG: New array and object on every render — defeats memo
<MemoizedChart
data={data.filter((d) => d.active)} // new array every render
style={{ marginTop: 10 }} // new object every render
colors={['red', 'blue', 'green']} // new array every render
/>CORRECT: Hoist constants and memoize computed values.
const CHART_STYLE = { marginTop: 10 } as const;
const CHART_COLORS = ['red', 'blue', 'green'] as const;
function Dashboard({ data }: { data: DataPoint[] }) {
const activeData = useMemo(
() => data.filter((d) => d.active),
[data]
);
return (
<MemoizedChart
data={activeData}
style={CHART_STYLE}
colors={CHART_COLORS}
/>
);
}---
Anti-Pattern 7: Declaring React.lazy Inside a Component
// WRONG: Creates a new lazy component on every render — causes remounting
function App() {
const Dashboard = lazy(() => import('./Dashboard')); // recreated every render!
return (
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
);
}WHY this is wrong: Each render creates a new component type. React sees it as a different component and unmounts/remounts it, losing all state and triggering a new import.
CORRECT: ALWAYS declare lazy at module level.
const Dashboard = lazy(() => import('./Dashboard'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
);
}---
Anti-Pattern 8: Rendering Thousands of DOM Nodes
// WRONG: 10,000 DOM nodes — browser becomes unresponsive
function UserList({ users }: { users: User[] }) {
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}WHY this is wrong: The browser must create, lay out, and paint 10,000+ DOM nodes. Scrolling becomes janky. Memory usage spikes. Initial render takes hundreds of milliseconds.
CORRECT: Use virtualization — render only the visible items plus a small overscan buffer. See @tanstack/react-virtual in the SKILL.md.
---
Anti-Pattern 9: Effect Chains for Derived State
// WRONG: Cascading effects cause multiple render cycles
const [items, setItems] = useState<Item[]>([]);
const [filteredItems, setFilteredItems] = useState<Item[]>([]);
const [count, setCount] = useState(0);
useEffect(() => {
setFilteredItems(items.filter((i) => i.active));
}, [items]);
useEffect(() => {
setCount(filteredItems.length);
}, [filteredItems]);WHY this is wrong: Three renders: (1) items change, (2) filteredItems updates, (3) count updates. Each effect triggers a new render cycle.
CORRECT: Calculate derived values during render.
const [items, setItems] = useState<Item[]>([]);
const filteredItems = useMemo(() => items.filter((i) => i.active), [items]);
const count = filteredItems.length;
// Single render with correct values---
Anti-Pattern 10: Misusing useMemo with Object Literal Syntax
// WRONG: Returns undefined — block body without return
const options = useMemo(() => {
matchMode: 'whole-word', text
}, [text]);
// CORRECT: Wrap object literal in parentheses
const options = useMemo(() => ({
matchMode: 'whole-word' as const,
text,
}), [text]);WHY this is wrong: JavaScript interprets { matchMode: ... } as a code block with a label, not an object literal. The function returns undefined.
---
Anti-Pattern 11: Using flushSync for Performance
// WRONG: flushSync breaks batching
import { flushSync } from 'react-dom';
function handleClick() {
flushSync(() => setCount((c) => c + 1));
flushSync(() => setFlag((f) => !f));
// TWO renders instead of one
}WHY this is wrong: flushSync forces synchronous DOM updates, breaking React's automatic batching. Each call causes a separate render cycle. ONLY use flushSync when you must read the DOM immediately after a state update (e.g., scrolling to a newly added element).
---
Summary Table
| Anti-Pattern | Problem | Fix |
|---|---|---|
| Memo everywhere | Comparison overhead > render cost | Measure first, then optimize |
| useMemo for trivial work | Overhead > calculation cost | Inline the calculation |
| useCallback without memo child | No effect, wasted overhead | Add memo or remove useCallback |
| JSON.stringify comparison | Slower than re-rendering | Use default shallow comparison |
| New objects in JSX props | Defeats memo | Hoist constants, useMemo for computed |
| lazy() inside component | Remounts every render | Declare at module level |
| Thousands of DOM nodes | Jank, high memory | Use @tanstack/react-virtual |
| Effect chains for derived state | Multiple render cycles | Calculate during render / useMemo |
| flushSync for batching | Breaks batching | Let React batch automatically |
react-impl-performance — Examples
Example 1: Optimizing a Filterable Product List
Before (unoptimized)
interface Product {
id: string;
name: string;
category: string;
price: number;
}
function ProductPage({ products }: { products: Product[] }) {
const [query, setQuery] = useState('');
const [sortBy, setSortBy] = useState<'name' | 'price'>('name');
// PROBLEM: Recalculates on EVERY render (even when query/sortBy unchanged)
const filtered = products
.filter((p) => p.name.toLowerCase().includes(query.toLowerCase()))
.sort((a, b) => (sortBy === 'name' ? a.name.localeCompare(b.name) : a.price - b.price));
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<button onClick={() => setSortBy('name')}>Sort by Name</button>
<button onClick={() => setSortBy('price')}>Sort by Price</button>
{/* PROBLEM: New function created every render, defeats any child memoization */}
<ProductList items={filtered} onSelect={(id) => console.log(id)} />
</div>
);
}
function ProductList({ items, onSelect }: { items: Product[]; onSelect: (id: string) => void }) {
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name} — ${item.price}
</li>
))}
</ul>
);
}After (optimized with React 18 manual memoization)
import { useState, useMemo, useCallback, memo } from 'react';
function ProductPage({ products }: { products: Product[] }) {
const [query, setQuery] = useState('');
const [sortBy, setSortBy] = useState<'name' | 'price'>('name');
// useMemo: only recalculate when products, query, or sortBy change
const filtered = useMemo(() => {
return products
.filter((p) => p.name.toLowerCase().includes(query.toLowerCase()))
.sort((a, b) => (sortBy === 'name' ? a.name.localeCompare(b.name) : a.price - b.price));
}, [products, query, sortBy]);
// useCallback: stable reference for memoized child
const handleSelect = useCallback((id: string) => {
console.log(id);
}, []);
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<button onClick={() => setSortBy('name')}>Sort by Name</button>
<button onClick={() => setSortBy('price')}>Sort by Price</button>
<ProductList items={filtered} onSelect={handleSelect} />
</div>
);
}
// memo: skip re-render when items and onSelect are referentially equal
const ProductList = memo<{ items: Product[]; onSelect: (id: string) => void }>(
function ProductList({ items, onSelect }) {
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name} — ${item.price}
</li>
))}
</ul>
);
}
);After (React 19 with React Compiler)
// No manual memo/useMemo/useCallback needed — the compiler handles it automatically.
function ProductPage({ products }: { products: Product[] }) {
const [query, setQuery] = useState('');
const [sortBy, setSortBy] = useState<'name' | 'price'>('name');
const filtered = products
.filter((p) => p.name.toLowerCase().includes(query.toLowerCase()))
.sort((a, b) => (sortBy === 'name' ? a.name.localeCompare(b.name) : a.price - b.price));
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<button onClick={() => setSortBy('name')}>Sort by Name</button>
<button onClick={() => setSortBy('price')}>Sort by Price</button>
<ProductList items={filtered} onSelect={(id) => console.log(id)} />
</div>
);
}
function ProductList({ items, onSelect }: { items: Product[]; onSelect: (id: string) => void }) {
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name} — ${item.price}
</li>
))}
</ul>
);
}---
Example 2: Profiler Usage for Measuring Before Optimizing
import { Profiler, useState } from 'react';
interface RenderLog {
id: string;
phase: string;
actualDuration: number;
baseDuration: number;
}
const renderLogs: RenderLog[] = [];
function onRender(
id: string,
phase: 'mount' | 'update' | 'nested-update',
actualDuration: number,
baseDuration: number,
): void {
renderLogs.push({ id, phase, actualDuration, baseDuration });
if (actualDuration > 16) {
console.warn(
`[Perf] ${id} took ${actualDuration.toFixed(1)}ms (budget: 16ms for 60fps)`
);
}
}
function App() {
return (
<Profiler id="Sidebar" onRender={onRender}>
<Sidebar />
</Profiler>
);
}---
Example 3: Route-Based Code Splitting
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// ALWAYS declare lazy components at module level
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const AdminPanel = lazy(() => import('./pages/AdminPanel'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/admin" element={<AdminPanel />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
function PageSkeleton() {
return <div className="skeleton-page" aria-busy="true">Loading...</div>;
}---
Example 4: Virtualizing a Large List
import { useRef } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
interface Row {
id: string;
name: string;
email: string;
}
function VirtualTable({ rows }: { rows: Row[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const rowVirtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48,
overscan: 5, // render 5 extra items above/below viewport for smooth scrolling
});
return (
<div
ref={parentRef}
style={{ height: '600px', overflow: 'auto' }}
>
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const row = rows[virtualRow.index];
return (
<div
key={row.id}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
display: 'flex',
alignItems: 'center',
borderBottom: '1px solid #eee',
}}
>
<span style={{ flex: 1 }}>{row.name}</span>
<span style={{ flex: 1 }}>{row.email}</span>
</div>
);
})}
</div>
</div>
);
}---
Example 5: Measuring and Comparing With/Without Memo
import { memo, useMemo, useCallback, Profiler, useState } from 'react';
// Step 1: Add Profiler to measure baseline
function App() {
const [count, setCount] = useState(0);
const [items] = useState(() => Array.from({ length: 5000 }, (_, i) => `Item ${i}`));
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>
Re-render parent (count: {count})
</button>
<Profiler id="ItemList" onRender={(id, phase, actual, base) => {
console.log(`${id}: actual=${actual.toFixed(2)}ms, base=${base.toFixed(2)}ms`);
}}>
<ItemList items={items} />
</Profiler>
</div>
);
}
// Step 2: Without memo — re-renders on every parent state change
function ItemList({ items }: { items: string[] }) {
return (
<ul>
{items.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
);
}
// Step 3: With memo — skips re-render because items reference is stable
const ItemList = memo<{ items: string[] }>(function ItemList({ items }) {
return (
<ul>
{items.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
);
});
// Compare Profiler output: actualDuration drops to ~0ms with memo
// because the component is skipped entirely.---
Example 6: Feature-Based Code Splitting with Dynamic Import
import { lazy, Suspense, useState } from 'react';
// Heavy chart library loaded only when user clicks "Show Chart"
const Chart = lazy(() => import('./components/Chart'));
function AnalyticsDashboard({ data }: { data: DataPoint[] }) {
const [showChart, setShowChart] = useState(false);
return (
<div>
<h2>Analytics</h2>
<table>{/* lightweight table always loaded */}</table>
<button onClick={() => setShowChart(true)}>Show Chart</button>
{showChart && (
<Suspense fallback={<div>Loading chart...</div>}>
<Chart data={data} />
</Suspense>
)}
</div>
);
}react-impl-performance — Patterns
Pattern 1: React Compiler Setup (React 19+)
The React Compiler replaces manual memo, useMemo, and useCallback with automatic build-time memoization.
Vite Setup
npm install -D babel-plugin-react-compiler@latest// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
babel: {
plugins: ['babel-plugin-react-compiler'],
},
}),
],
});Vite with vite-plugin-babel
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import babel from 'vite-plugin-babel';
export default defineConfig({
plugins: [
react(),
babel({
babelConfig: {
plugins: ['babel-plugin-react-compiler'],
},
}),
],
});Babel (generic)
// babel.config.js
module.exports = {
plugins: [
'babel-plugin-react-compiler', // MUST be first
// ... other plugins
],
};Next.js
See https://nextjs.org/docs/app/api-reference/next-config-js/reactCompiler
Webpack (community)
See https://github.com/SukkaW/react-compiler-webpack
Configuration Options
const ReactCompilerConfig = {
compilationMode: 'all', // 'annotation' | 'infer' | 'all'
target: '19', // '17' | '18' | '19'
panicThreshold: 'none', // skip errors instead of failing build
};
// Pass as second element in plugin array:
plugins: [['babel-plugin-react-compiler', ReactCompilerConfig]];| Mode | Behavior |
|---|---|
'all' | Compile every component and hook |
'infer' | Compiler decides what to optimize |
'annotation' | Only compile functions with "use memo" directive |
ESLint Integration
npm install -D eslint-plugin-react-hooks@latestALWAYS install the ESLint plugin. It identifies Rules of React violations that prevent the compiler from optimizing components.
Gradual Adoption with "use memo" and "use no memo"
// Opt in a single component (when compilationMode is 'annotation')
function OptimizedComponent() {
"use memo";
return <div>Compiled</div>;
}
// Opt out a single component (any compilationMode)
function SkippedComponent() {
"use no memo";
return <div>Not compiled</div>;
}Directives can be placed at module level (top of file, before imports) to apply to all functions in that file.
---
Pattern 2: Stable Props for Memoized Children
When passing props to a memo-wrapped child, ALWAYS ensure referential stability.
Strategy: Pass primitives instead of objects
// BAD: New object every render
<Profile person={{ name, age }} />
// GOOD: Primitives are stable by value
<Profile name={name} age={age} />Strategy: useMemo for object props
const person = useMemo(() => ({ name, age }), [name, age]);
<MemoizedProfile person={person} />Strategy: useCallback for function props
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
<MemoizedChild onClick={handleClick} />Strategy: Derive minimal data
// BAD: Entire user object causes re-render when any field changes
<CallToAction user={user} />
// GOOD: Pass only what's needed
const hasGroups = user.groups !== null;
<CallToAction hasGroups={hasGroups} />---
Pattern 3: Context Performance Optimization
Context changes re-render ALL consumers. Split contexts to minimize unnecessary re-renders.
Split static and dynamic values
// BAD: Every consumer re-renders when either value changes
const AppContext = createContext<{ theme: string; user: User | null }>({
theme: 'light',
user: null,
});
// GOOD: Separate contexts — theme consumers don't re-render on user change
const ThemeContext = createContext<string>('light');
const UserContext = createContext<User | null>(null);Memoize the context value
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState('light');
// useMemo prevents unnecessary re-renders of consumers
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return <ThemeContext value={value}>{children}</ThemeContext>;
}Move state down (lift content up)
// BAD: ExpensiveTree re-renders on every color change
function App() {
const [color, setColor] = useState('red');
return (
<div style={{ color }}>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<ExpensiveTree />
</div>
);
}
// GOOD: Extract the state-dependent part
function App() {
return (
<ColorPicker>
<ExpensiveTree />
</ColorPicker>
);
}
function ColorPicker({ children }: { children: React.ReactNode }) {
const [color, setColor] = useState('red');
return (
<div style={{ color }}>
<input value={color} onChange={(e) => setColor(e.target.value)} />
{children} {/* children is a stable reference, does not re-render */}
</div>
);
}---
Pattern 4: Lazy Loading with Preload Hints
Combine React.lazy with module preloading for better UX.
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
// Preload on hover — starts loading before the user clicks
function NavLink() {
const preload = () => {
import('./pages/Dashboard');
};
return (
<a href="/dashboard" onMouseEnter={preload} onFocus={preload}>
Dashboard
</a>
);
}---
Pattern 5: Debounced Input with Deferred Value
Use useDeferredValue for non-urgent updates (keeps input responsive).
import { useState, useDeferredValue, useMemo } from 'react';
function SearchPage({ items }: { items: string[] }) {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// Expensive filter uses deferred value — input stays responsive
const results = useMemo(
() => items.filter((item) => item.includes(deferredQuery)),
[items, deferredQuery]
);
const isStale = query !== deferredQuery;
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<ResultList results={results} />
</div>
</div>
);
}---
Pattern 6: Image Optimization
Lazy loading images
function ProductImage({ src, alt }: { src: string; alt: string }) {
return (
<img
src={src}
alt={alt}
loading="lazy" // Browser-native lazy loading
decoding="async" // Non-blocking decode
width={300} // ALWAYS set dimensions to prevent layout shift
height={200}
/>
);
}Responsive images with srcSet
function ResponsiveImage({ baseSrc, alt }: { baseSrc: string; alt: string }) {
return (
<img
src={`${baseSrc}-800.webp`}
srcSet={`${baseSrc}-400.webp 400w, ${baseSrc}-800.webp 800w, ${baseSrc}-1200.webp 1200w`}
sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px"
alt={alt}
loading="lazy"
decoding="async"
width={800}
height={600}
/>
);
}ALWAYS set explicit width and height to prevent Cumulative Layout Shift (CLS).
---
Pattern 7: Batch State Updates
React 18+ automatically batches state updates in event handlers, promises, setTimeout, and native event handlers. No manual batching needed.
// React 18+: Both updates result in a SINGLE re-render
function handleClick() {
setCount((c) => c + 1);
setFlag((f) => !f);
// React batches these — one render, not two
}
// Also batched in async contexts (new in React 18)
async function handleSubmit() {
const data = await fetchData();
setData(data);
setLoading(false);
// Single re-render
}NEVER use flushSync unless you need the DOM updated before the next line (e.g., measuring after a state change). It breaks batching and hurts performance.