Performance Optimization Skill
When to Use This Skill
- Components re-rendering too frequently
- App feels slow or laggy
- Large bundle size warnings
- Lists with many items performing poorly
- Before shipping to production
Rule 1 — Wrap Expensive Components in React.memo
// ❌ Re-renders whenever parent re-renders
function ExpensiveChart({ data, title }) {
return <div>{/* expensive render */}</div>;
}
// ✅ Only re-renders when data or title changes
const ExpensiveChart = React.memo(function ExpensiveChart({ data, title }) {
return <div>{/* expensive render */}</div>;
});
// ✅ With custom comparison for complex objects
const ExpensiveChart = React.memo(
function ExpensiveChart({ data, title }) {
return <div>{/* expensive render */}</div>;
},
(prevProps, nextProps) => prevProps.data.id === nextProps.data.id
);
AI instruction: Any component that receives props and does significant work SHOULD be wrapped in React.memo.
Rule 2 — Stable References with useCallback
// ❌ New function reference on every render — breaks memo on child
function Parent() {
const handleClick = () => console.log('clicked');
return <MemoChild />;
}
// ✅ Stable reference — memo actually works
function Parent() {
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // deps: only recreate when these change
return <MemoChild />;
}
AI instruction: Event handlers passed as props MUST use useCallback when the receiving component uses React.memo.
Rule 3 — Memoize Expensive Calculations
// ❌ Recalculates on every render
function ProductList({ products, filters }) {
const filtered = products
.filter(p => filters.category === p.category)
.sort((a, b) => b.price - a.price);
return <List items={filtered} />;
}
// ✅ Only recalculates when products or filters change
function ProductList({ products, filters }) {
const filtered = useMemo(() =>
products
.filter(p => filters.category === p.category)
.sort((a, b) => b.price - a.price),
[products, filters]
);
return <List items={filtered} />;
}
AI instruction: Any array/object derived from props that's used in JSX or passed as props SHOULD use useMemo if the calculation is non-trivial.
Rule 4 — Never Create Objects/Arrays Inline in JSX
// ❌ New object reference every render — breaks child memo
<Component style={{ margin: 0, padding: 0 }} options={['a', 'b']} />
// ✅ Move outside component (if static)
const STYLE = { margin: 0, padding: 0 };
const OPTIONS = ['a', 'b'];
<Component style={STYLE} options={OPTIONS} />
// ✅ Or useMemo if dynamic
const style = useMemo(() => ({ margin: 0, padding: count }), [count]);
Rule 5 — Virtualize Long Lists
// ❌ Renders all 10,000 items — kills performance
function HugeList({ items }) {
return (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}
// ✅ Only renders visible items
import { FixedSizeList } from 'react-window';
function HugeList({ items }) {
return (
<FixedSizeList height={600} itemCount={items.length} itemSize={35}>
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>
);
}
AI instruction: Lists with more than 100 items SHOULD use react-window or react-virtual.
Rule 6 — Lazy Load Routes and Heavy Components
// ❌ All routes loaded upfront — large initial bundle
import Dashboard from './Dashboard';
import Analytics from './Analytics';
import Reports from './Reports';
// ✅ Load only when needed
const Dashboard = lazy(() => import('./Dashboard'));
const Analytics = lazy(() => import('./Analytics'));
const Reports = lazy(() => import('./Reports'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Suspense>
);
}
Rule 7 — Correct useEffect Dependencies
// ❌ Missing deps — stale closure bug
useEffect(() => {
fetchData(userId);
}, []); // userId never updates
// ❌ Too many deps — runs too often
useEffect(() => {
fetchData(userId);
}, [userId, otherThing, anotherThing]);
// ✅ Only what the effect actually uses
useEffect(() => {
fetchData(userId);
}, [userId]);
Rule 8 — State Updates Batching
// ❌ Two re-renders (React 17 and below in event handlers)
function handleClick() {
setCount(c => c + 1);
setName('updated');
}
// ✅ One re-render (React 18 auto-batches, but explicit is safer)
import { flushSync } from 'react-dom';
// Or just trust React 18 automatic batching
Companion Script
npx reactforge performance ./src
npx reactforge detect-slow ./src
Quick Checklist
- Components receiving props wrapped in React.memo
- Event handlers in props use useCallback
- Expensive calculations use useMemo
- No inline objects/arrays passed as props
- Lists > 100 items use virtualisation
- Routes use React.lazy + Suspense
- useEffect dependency arrays are correct and minimal