React and Next.js performance optimization guidelines from Vercel Engineering. Contains 57 rules across 8 categories, prioritized by impact. Use when writing React components, implementing data fetching, reviewing code for performance, or optimizing bundle size.
Performance optimization guidelines with 57 rules in 8 categories, prioritized by impact.
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Eliminating Waterfalls | CRITICAL | async- |
| 2 | Bundle Size Optimization | CRITICAL | bundle- |
| 3 | Server-Side Performance | HIGH | server- |
| 4 | Client-Side Data Fetching |
| MEDIUM-HIGH |
client- |
| 5 | Re-render Optimization | MEDIUM | rerender- |
| 6 | Rendering Performance | MEDIUM | rendering- |
| 7 | JavaScript Performance | LOW-MEDIUM | js- |
| 8 | Advanced Patterns | LOW | advanced- |
Rule: Move await into branches where actually used
❌ Bad:
async function getData() {
const user = await fetchUser(); // Blocks everything
const posts = await fetchPosts();
const comments = await fetchComments();
return { user, posts, comments };
}
✅ Good:
async function getData() {
const userPromise = fetchUser();
const postsPromise = fetchPosts();
const commentsPromise = fetchComments();
const [user, posts, comments] = await Promise.all([
userPromise,
postsPromise,
commentsPromise
]);
return { user, posts, comments };
}
Rule: Use Promise.all() for independent operations
Rule: Use better-all for partial dependencies (start fetches early)
Rule: Start promises early, await late in API routes
// Start immediately
const userPromise = fetchUser(userId);
const settingsPromise = fetchSettings(userId);
// Do other work...
validateRequest(req);
// Await at the end
const [user, settings] = await Promise.all([userPromise, settingsPromise]);
Rule: Use Suspense to stream content progressively
<Suspense fallback={<Skeleton />}>
<SlowComponent />
</Suspense>
Rule: Import directly, avoid barrel files
❌ Bad:
import { Button, Input, Card } from '@/components'; // Imports everything
✅ Good:
import { Button } from '@/components/button';
import { Input } from '@/components/input';
Rule: Use next/dynamic for heavy components
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('./HeavyChart'), {
ssr: false,
loading: () => <Skeleton />
});
Rule: Load analytics/logging after hydration
import Script from 'next/script';
<Script
src="https://analytics.com/script.js"
strategy="afterInteractive" // or "lazyOnload"
/>
Rule: Load modules only when feature is activated
Rule: Preload on hover/focus for perceived speed
const linkRef = useRef<HTMLAnchorElement>(null);
const onMouseEnter = () => {
const href = linkRef.current?.href;
if (href) {
router.prefetch(href);
}
};
Rule: Authenticate server actions like API routes
Rule: Use React.cache() for per-request deduplication
import { cache } from 'react';
const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});
// Called multiple times, but only one DB query
const user = await getUser(id);
const sameUser = await getUser(id);
Rule: Use LRU cache for cross-request caching
import { LRUCache } from 'lru-cache';
const cache = new LRUCache({
max: 500,
ttl: 1000 * 60 * 5, // 5 minutes
});
export async function getCachedData(key: string) {
if (cache.has(key)) {
return cache.get(key);
}
const data = await fetchData(key);
cache.set(key, data);
return data;
}
Rule: Avoid duplicate serialization in RSC props
Rule: Minimize data passed to client components
❌ Bad:
<ClientComponent user={fullUserObjectWithPassword} />
✅ Good:
<ClientComponent user={pick(fullUser, ['id', 'name', 'email'])} />
Rule: Restructure components to parallelize fetches
Rule: Use after() for non-blocking operations
import { after } from 'next/server';
export default async function Page() {
const data = await fetchData();
after(async () => {
await logAnalytics(data.id); // Non-blocking
});
return <View data={data} />;
}
Rule: Use SWR/TanStack Query for automatic request deduplication
const { data } = useSWR('/api/user', fetcher, {
dedupingInterval: 2000, // 2 seconds
});
Rule: Deduplicate global event listeners
// Use a single window listener, not per-component
useEffect(() => {
const handler = () => setIsMobile(window.innerWidth < 768);
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);
Rule: Use passive listeners for scroll
element.addEventListener('scroll', handler, { passive: true });
Rule: Version and minimize localStorage data
Rule: Don't subscribe to state only used in callbacks
Rule: Extract expensive work into memoized components
const ExpensiveList = memo(function ExpensiveList({ items }) {
return (
<ul>
{items.map(item => (
<ExpensiveItem key={item.id} item={item} />
))}
</ul>
);
});
Rule: Hoist default non-primitive props
❌ Bad:
function Button({ items = [] }) { // New array every render
// ...
}
✅ Good:
const DEFAULT_ITEMS: string[] = [];
function Button({ items = DEFAULT_ITEMS }) {
// ...
}
Rule: Use primitive dependencies in effects
❌ Bad:
useEffect(() => {
fetchData(filters);
}, [filters]); // Object, triggers every render
✅ Good:
useEffect(() => {
fetchData(filters);
}, [filters.status, filters.page]); // Primitives
Rule: Subscribe to derived booleans, not raw values
// Instead of checking array length in render
const hasItems = items.length > 0; // Computed once
Rule: Derive state during render, not effects
❌ Bad:
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
✅ Good:
const fullName = `${firstName} ${lastName}`; // Derive in render
Rule: Use functional setState for stable callbacks
setCount(prev => prev + 1); // Always correct
Rule: Pass function to useState for expensive values
const [data] = useState(() => computeExpensiveData()); // Once
Rule: Avoid memo for simple primitives
❌ Bad:
const isActive = useMemo(() => status === 'active', [status]);
✅ Good:
const isActive = status === 'active'; // Simple comparison
Rule: Put interaction logic in event handlers
// Instead of useEffect that responds to state changes
const handleClick = () => {
setCount(c => c + 1);
logAnalytics('button_click'); // In event, not effect
};
Rule: Use startTransition for non-urgent updates
import { useTransition } from 'react';
const [isPending, startTransition] = useTransition();
const handleFilter = (newFilter) => {
startTransition(() => {
setFilter(newFilter); // Non-urgent
});
};
Rule: Use refs for transient frequent values
const mousePos = useRef({ x: 0, y: 0 });
useEffect(() => {
const handler = (e) => {
mousePos.current = { x: e.clientX, y: e.clientY };
};
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);
Rule: Animate div wrapper, not SVG element
❌ Bad:
<svg style={{ transform: 'scale(1.5)' }}> // Slow
✅ Good:
<div style={{ transform: 'scale(1.5)' }}>
<svg> // Unchanged
Rule: Use content-visibility for long lists
.list-item {
content-visibility: auto;
contain-intrinsic-size: 0 100px;
}
Rule: Extract static JSX outside components
const StaticIcon = <svg>...</svg>; // Defined once
function Button() {
return <button>{StaticIcon}</button>; // Reused
}
Rule: Reduce SVG coordinate precision
// Round to 2 decimal places
const d = `M ${x.toFixed(2)} ${y.toFixed(2)}`;
Rule: Use inline script for client-only data
Rule: Suppress expected mismatches
<span suppressHydrationWarning>
{new Date().toLocaleString()}
</span>
Rule: Use Activity component for show/hide
Rule: Use ternary, not && for conditionals
❌ Bad:
{items.length && <List items={items} />} // Renders "0"
✅ Good:
{items.length > 0 ? <List items={items} /> : null}
Rule: Prefer useTransition for loading state
Rule: Group CSS changes via classes or cssText
❌ Bad:
element.style.width = '100px';
element.style.height = '100px';
element.style.margin = '10px'; // 3 reflows
✅ Good:
element.className = 'size-100 m-10'; // 1 reflow
Rule: Build Map for repeated lookups
const userMap = new Map(users.map(u => [u.id, u]));
// O(1) lookup instead of O(n) find
const user = userMap.get(id);
Rule: Cache object properties in loops
for (let i = 0, len = array.length; i < len; i++) {
// len cached, not accessed every iteration
}
Rule: Cache function results in module-level Map
Rule: Cache localStorage/sessionStorage reads
Rule: Combine multiple filter/map into one loop
❌ Bad:
const filtered = items.filter(x => x.active);
const mapped = filtered.map(x => x.name);
✅ Good:
const result: string[] = [];
for (const item of items) {
if (item.active) {
result.push(item.name);
}
}
Rule: Check array length before expensive comparison
Rule: Return early from functions
Rule: Hoist RegExp creation outside loops
Rule: Use loop for min/max instead of sort
Rule: Use Set/Map for O(1) lookups
const validIds = new Set(ids); // O(n) once
if (validIds.has(id)) { // O(1) lookup
// ...
}
Rule: Use toSorted() for immutability
const sorted = items.toSorted((a, b) => a - b); // New array
Rule: Store event handlers in refs
Rule: Initialize app once per app load
Rule: useLatest for stable callback refs
| Rule | Apply to |
|---|---|
async-parallel | Image upload processing |
bundle-dynamic-imports | Heavy image preview components |
server-cache-react | Metadata profile fetching |
rerender-memo | Image grid, thumbnail lists |
rendering-content-visibility | Large image galleries |
js-set-map-lookups | Image ID lookups |