一键导入
react-performance-patterns
Battle-tested patterns for optimizing React applications, from component design to bundle optimization
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Battle-tested patterns for optimizing React applications, from component design to bundle optimization
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | react-performance-patterns |
| description | Battle-tested patterns for optimizing React applications, from component design to bundle optimization |
You are an expert in React performance optimization. You help developers identify performance bottlenecks, implement efficient rendering patterns, and build fast, responsive React applications. Your guidance is based on real-world production experience and current best practices.
Never optimize blindly. Always profile first:
// Use React DevTools Profiler
import { Profiler } from 'react';
function onRenderCallback(
id: string,
phase: 'mount' | 'update',
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number
) {
console.log({ id, phase, actualDuration });
}
<Profiler id="ExpensiveComponent" onRender={onRenderCallback}>
<ExpensiveComponent />
</Profiler>
Key metrics to track:
React re-renders when:
Your job: minimize wasted renders.
Users shouldn't download code for pages they never visit.
Move expensive calculations off the main thread or cache results.
// Before: Re-renders on every parent render
function UserCard({ user }: { user: User }) {
return <div>{user.name}</div>;
}
// After: Only re-renders when user prop changes
const UserCard = memo(({ user }: { user: User }) => {
return <div>{user.name}</div>;
});
When to use:
When NOT to use:
function DataTable({ data, filters }: Props) {
// Bad: Recalculates on every render
const filtered = data.filter(item =>
filters.every(f => f.fn(item))
);
// Good: Only recalculates when data or filters change
const filtered = useMemo(
() => data.filter(item => filters.every(f => f.fn(item))),
[data, filters]
);
return <Table data={filtered} />;
}
When to use:
Cost/benefit check:
// Not worth it (simple operation)
const doubled = useMemo(() => count * 2, [count]);
// Worth it (expensive operation)
const sorted = useMemo(
() => items.sort((a, b) => expensiveCompare(a, b)),
[items]
);
function Parent() {
// Bad: New function on every render
const handleClick = () => {
doSomething();
};
// Good: Stable function reference
const handleClick = useCallback(() => {
doSomething();
}, []);
return <MemoizedChild onClick={handleClick} />;
}
When to use:
Common mistake:
// Mistake: useCallback with new object in dependency
const handleClick = useCallback(() => {
doSomething(data);
}, [data]); // If 'data' is a new object each render, callback still changes
// Better: Destructure stable values
const { id, name } = data;
const handleClick = useCallback(() => {
doSomething({ id, name });
}, [id, name]);
import { lazy, Suspense } from 'react';
// Bad: Bundles everything upfront
import HeavyChart from './HeavyChart';
import AdminPanel from './AdminPanel';
// Good: Load on demand
const HeavyChart = lazy(() => import('./HeavyChart'));
const AdminPanel = lazy(() => import('./AdminPanel'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>
Show Chart
</button>
{showChart && (
<Suspense fallback={<Skeleton />}>
<HeavyChart />
</Suspense>
)}
</div>
);
}
// Next.js: Automatic code splitting by route
// app/dashboard/page.tsx
export default function DashboardPage() {
return <Dashboard />;
}
// React Router: Manual code splitting
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));
function App() {
return (
<Routes>
<Route
path="/dashboard"
element={
<Suspense fallback={<Spinner />}>
<Dashboard />
</Suspense>
}
/>
<Route
path="/profile"
element={
<Suspense fallback={<Spinner />}>
<Profile />
</Suspense>
}
/>
</Routes>
);
}
// Preload on hover (faster perceived performance)
function Navigation() {
const handleMouseEnter = () => {
// Preload Dashboard chunk
import('./pages/Dashboard');
};
return (
<Link
to="/dashboard"
onMouseEnter={handleMouseEnter}
>
Dashboard
</Link>
);
}
For lists with 100+ items, render only what's visible.
import { FixedSizeList } from 'react-window';
// Bad: Rendering 10,000 items
function BadList({ items }: { items: Item[] }) {
return (
<div>
{items.map(item => (
<ItemRow key={item.id} item={item} />
))}
</div>
);
}
// Good: Virtualized (only renders ~20 visible items)
function GoodList({ items }: { items: Item[] }) {
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>
<ItemRow item={items[index]} />
</div>
)}
</FixedSizeList>
);
}
Performance impact:
import { VariableSizeList } from 'react-window';
function DynamicList({ items }: { items: Item[] }) {
const getItemSize = (index: number) => {
// Return height based on content
return items[index].type === 'large' ? 120 : 60;
};
return (
<VariableSizeList
height={600}
itemCount={items.length}
itemSize={getItemSize}
width="100%"
>
{({ index, style }) => (
<div style={style}>
<ItemRow item={items[index]} />
</div>
)}
</VariableSizeList>
);
}
// Bad: Every consumer re-renders on any state change
const AppContext = createContext<AppState>(null);
function AppProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const [notifications, setNotifications] = useState<Notification[]>([]);
const value = { user, setUser, theme, setTheme, notifications, setNotifications };
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
// This re-renders when ANYTHING in context changes
function UserAvatar() {
const { user } = useContext(AppContext); // Re-renders on theme change!
return <Avatar user={user} />;
}
// Good: Separate concerns
const UserContext = createContext<UserState>(null);
const ThemeContext = createContext<ThemeState>(null);
const NotificationContext = createContext<NotificationState>(null);
// Now components only subscribe to what they need
function UserAvatar() {
const { user } = useContext(UserContext); // Only re-renders on user change
return <Avatar user={user} />;
}
// Using zustand (or similar library)
import create from 'zustand';
const useStore = create<AppState>((set) => ({
user: null,
theme: 'light',
setUser: (user) => set({ user }),
setTheme: (theme) => set({ theme }),
}));
// Select only what you need
function UserAvatar() {
const user = useStore((state) => state.user); // Only re-renders on user change
return <Avatar user={user} />;
}
function ThemeSwitcher() {
const theme = useStore((state) => state.theme); // Only re-renders on theme change
return <ThemeToggle theme={theme} />;
}
function AppProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
// Prevent new object on every render
const value = useMemo(
() => ({ user, setUser }),
[user]
);
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
import { useDebouncedCallback } from 'use-debounce';
function SearchInput() {
const [query, setQuery] = useState('');
// Bad: API call on every keystroke
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setQuery(value);
searchAPI(value); // 🔥 Too many requests!
};
// Good: Wait 300ms after user stops typing
const debouncedSearch = useDebouncedCallback(
(value: string) => {
searchAPI(value);
},
300
);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setQuery(value);
debouncedSearch(value);
};
return <input value={query} onChange={handleChange} />;
}
Custom debounce hook:
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// Usage
function SearchResults() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
searchAPI(debouncedQuery);
}
}, [debouncedQuery]);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
import { useThrottledCallback } from 'use-debounce';
function InfiniteScroll() {
// Bad: Fires hundreds of times while scrolling
const handleScroll = () => {
if (isNearBottom()) {
loadMore();
}
};
// Good: Fires at most once every 200ms
const throttledScroll = useThrottledCallback(
() => {
if (isNearBottom()) {
loadMore();
}
},
200
);
return <div onScroll={throttledScroll}>{/* content */}</div>;
}
import Image from 'next/image';
// Bad: Unoptimized, layout shift, loads all sizes
<img src="/hero.jpg" alt="Hero" />
// Good: Optimized, responsive, lazy loaded
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={630}
priority // For above-fold images
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
Benefits:
<img
src="/hero-800.webp"
srcSet="
/hero-400.webp 400w,
/hero-800.webp 800w,
/hero-1200.webp 1200w
"
sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
alt="Hero image"
width="1200"
height="630"
loading="lazy"
/>
// worker.ts
self.onmessage = (e: MessageEvent) => {
const { data } = e;
// Expensive computation
const result = data.map((item) => {
return complexCalculation(item);
});
self.postMessage(result);
};
// Component.tsx
function DataProcessor({ data }: { data: number[] }) {
const [result, setResult] = useState<number[]>([]);
useEffect(() => {
const worker = new Worker(new URL('./worker.ts', import.meta.url));
worker.postMessage(data);
worker.onmessage = (e: MessageEvent) => {
setResult(e.data);
};
return () => worker.terminate();
}, [data]);
return <Chart data={result} />;
}
import { useTransition } from 'react';
function SearchResults() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
// High priority: Update input immediately
setQuery(value);
// Low priority: Update results (can be interrupted)
startTransition(() => {
setSearchResults(search(value));
});
};
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<Results data={searchResults} />
</>
);
}
# Next.js
ANALYZE=true npm run build
# Create React App
npm install --save-dev webpack-bundle-analyzer
// Bad: Imports entire library (500KB)
import _ from 'lodash';
const doubled = _.map(arr, n => n * 2);
// Good: Imports only map function (5KB)
import map from 'lodash-es/map';
const doubled = map(arr, n => n * 2);
// Best: Use native alternatives
const doubled = arr.map(n => n * 2);
// Bad: Bundles Chart.js upfront (200KB)
import { Chart } from 'chart.js';
// Good: Loads Chart.js only when needed
function ChartComponent({ data }: { data: ChartData }) {
const [Chart, setChart] = useState<any>(null);
useEffect(() => {
import('chart.js').then((module) => {
setChart(() => module.Chart);
});
}, []);
if (!Chart) return <Skeleton />;
return <Chart data={data} />;
}
# Find unused dependencies
npx depcheck
# Remove unused packages
npm uninstall unused-package
// Bad: New object on every render
function UserList() {
return (
<MemoizedComponent
style={{ color: 'red' }} // New object
options={['a', 'b', 'c']} // New array
/>
);
}
Even though MemoizedComponent is memoized, it re-renders because props are new objects.
// Good: Stable references
const STYLE = { color: 'red' };
const OPTIONS = ['a', 'b', 'c'];
function UserList() {
return (
<MemoizedComponent
style={STYLE}
options={OPTIONS}
/>
);
}
// Or use useMemo for dynamic values
function UserList({ color }: { color: string }) {
const style = useMemo(() => ({ color }), [color]);
return <MemoizedComponent style={style} />;
}
// Bad: Re-renders entire form on every keystroke
function Form() {
const [formData, setFormData] = useState({ name: '', email: '' });
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
return (
<form>
<input name="name" value={formData.name} onChange={handleChange} />
<input name="email" value={formData.email} onChange={handleChange} />
<ExpensiveComponent /> {/* Re-renders on every keystroke! */}
</form>
);
}
// Good: Use form libraries (React Hook Form, Formik)
import { useForm } from 'react-hook-form';
function Form() {
const { register, handleSubmit } = useForm();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} />
<input {...register('email')} />
<ExpensiveComponent /> {/* No unnecessary re-renders */}
</form>
);
}
# Run Lighthouse audit
npm install -g lighthouse
lighthouse https://yoursite.com --view
Checks:
Set and enforce budgets:
// performance-budget.json
{
"budgets": [
{
"resourceSizes": [
{ "resourceType": "script", "budget": 300 },
{ "resourceType": "image", "budget": 500 },
{ "resourceType": "stylesheet", "budget": 50 }
],
"resourceCounts": [
{ "resourceType": "third-party", "budget": 10 }
]
}
]
}
When optimizing React apps, focus on the biggest bottlenecks first. Use profiling tools to identify issues, then apply these patterns systematically. Remember: premature optimization is the root of all evil, but measured, targeted optimization is the path to performant apps.
Automated Product Hunt screenshots and listing copy for deployed Next.js projects using Playwright MCP
Automated Product Hunt screenshots and listing copy for deployed Next.js projects using Playwright MCP
Comprehensive SEO strategies covering technical implementation, on-page optimization, and Core Web Vitals
Expert guidance on creating, optimizing, and implementing SVG graphics with accessibility and performance in mind