| name | next-best-practices |
| description | React and Next.js performance optimization patterns from Vercel Engineering |
| user-invocable | true |
| allowed-tools | Read, Write, Glob, Grep, Edit |
Next.js Best Practices
Comprehensive performance optimization guide for React and Next.js applications, based on Vercel Engineering guidelines. Contains 57 rules across 8 categories, prioritized by impact.
Trigger
/next
Overview
Reference these guidelines when:
- Writing new React components or Next.js pages
- Implementing data fetching (client or server-side)
- Reviewing code for performance issues
- Refactoring existing React/Next.js code
- Optimizing bundle size or load times
Rule Categories by Priority
| Priority | Category | Impact | Rules |
|---|
| 1 | Eliminating Waterfalls | CRITICAL | 5 |
| 2 | Bundle Size Optimization | CRITICAL | 5 |
| 3 | Server-Side Performance | HIGH | 7 |
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | 4 |
| 5 | Re-render Optimization | MEDIUM | 12 |
| 6 | Rendering Performance | MEDIUM | 9 |
| 7 | JavaScript Performance | LOW-MEDIUM | 12 |
| 8 | Advanced Patterns | LOW | 3 |
Workflow
Phase 1: Identify Performance Issues
Start with CRITICAL categories first:
- Waterfalls - Sequential async operations
- Bundle Size - Large JavaScript bundles
- Server Performance - Slow server-side rendering
Phase 2: Apply Rules by Category
1. Eliminating Waterfalls (CRITICAL)
Sequential async operations are the #1 cause of slow pages.
async-parallel
Use Promise.all() for independent operations:
const user = await getUser(id);
const posts = await getPosts(id);
const comments = await getComments(id);
const [user, posts, comments] = await Promise.all([
getUser(id),
getPosts(id),
getComments(id),
]);
async-defer-await
Move await into branches where actually used:
async function getData(shouldFetch: boolean) {
const data = await fetchData();
if (shouldFetch) {
return data;
}
return null;
}
async function getData(shouldFetch: boolean) {
if (shouldFetch) {
return await fetchData();
}
return null;
}
async-suspense-boundaries
Use Suspense to stream content:
<Suspense fallback={<HeaderSkeleton />}>
<Header />
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<Content />
</Suspense>
2. Bundle Size Optimization (CRITICAL)
bundle-barrel-imports
Import directly, avoid barrel files:
import { Button } from "@/components";
import { Button } from "@/components/button";
bundle-dynamic-imports
Use next/dynamic for heavy components:
import HeavyChart from "@/components/heavy-chart";
const HeavyChart = dynamic(() => import("@/components/heavy-chart"), {
loading: () => <ChartSkeleton />,
});
bundle-defer-third-party
Load analytics/logging after hydration:
useEffect(() => {
import("@/lib/analytics").then((mod) => mod.init());
}, []);
bundle-conditional
Load modules only when feature is activated:
const handleExport = async () => {
const { exportToPDF } = await import("@/lib/pdf-export");
exportToPDF(data);
};
3. Server-Side Performance (HIGH)
server-cache-react
Use React.cache() for per-request deduplication:
import { cache } from "react";
export const getUser = cache(async (id: string) => {
return await db.user.findUnique({ where: { id } });
});
server-parallel-fetching
Restructure components to parallelize fetches:
async function Page() {
const user = await getUser();
const posts = await getPosts(user.id);
return <Content user={user} posts={posts} />;
}
async function Page() {
const user = await getUser();
return (
<>
<UserInfo user={user} />
<Suspense fallback={<PostsSkeleton />}>
<Posts userId={user.id} />
</Suspense>
</>
);
}
server-serialization
Minimize data passed to client components:
<ClientComponent data={fullUserObject} />
<ClientComponent name={user.name} avatar={user.avatar} />
4. Client-Side Data Fetching (MEDIUM-HIGH)
client-swr-dedup
Use SWR for automatic request deduplication:
import useSWR from "swr";
function UserProfile({ userId }) {
const { data: user } = useSWR(`/api/users/${userId}`, fetcher);
return <div>{user?.name}</div>;
}
client-passive-event-listeners
Use passive listeners for scroll:
window.addEventListener("scroll", handleScroll, { passive: true });
5. Re-render Optimization (MEDIUM)
rerender-memo
Extract expensive work into memoized components:
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<ExpensiveList items={items} /> {/* Re-renders unnecessarily */}
</div>
);
}
const MemoizedList = memo(ExpensiveList);
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<MemoizedList items={items} />
</div>
);
}
rerender-lazy-state-init
Pass function to useState for expensive initial values:
const [data, setData] = useState(expensiveComputation());
const [data, setData] = useState(() => expensiveComputation());
rerender-functional-setstate
Use functional setState for stable callbacks:
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);
const increment = useCallback(() => {
setCount((c) => c + 1);
}, []);
rerender-transitions
Use startTransition for non-urgent updates:
import { startTransition } from "react";
function handleSearch(query: string) {
setInputValue(query);
startTransition(() => {
setSearchResults(filterResults(query));
});
}
6. Rendering Performance (MEDIUM)
rendering-content-visibility
Use content-visibility for long lists:
.list-item {
content-visibility: auto;
contain-intrinsic-size: 0 100px;
}
rendering-conditional-render
Use ternary, not && for conditionals:
{count && <Counter count={count} />}
{count > 0 ? <Counter count={count} /> : null}
rendering-hydration-no-flicker
Use inline script for client-only data:
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
const theme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', theme);
})();
`,
}}
/>
7. JavaScript Performance (LOW-MEDIUM)
js-set-map-lookups
Use Set/Map for O(1) lookups:
const ids = [1, 2, 3, 4, 5];
items.filter(item => ids.includes(item.id));
const idSet = new Set([1, 2, 3, 4, 5]);
items.filter(item => idSet.has(item.id));
js-early-exit
Return early from functions:
function process(data) {
if (data) {
if (data.valid) {
}
}
}
function process(data) {
if (!data) return;
if (!data.valid) return;
}
js-combine-iterations
Combine multiple filter/map into one loop:
const filtered = items.filter(x => x.active);
const mapped = filtered.map(x => x.name);
const result = items.reduce((acc, x) => {
if (x.active) acc.push(x.name);
return acc;
}, []);
Quick Reference Card
CRITICAL (fix immediately):
├── Promise.all() for parallel operations
├── Import directly, not from barrels
├── next/dynamic for heavy components
├── Defer third-party scripts
└── Suspense for streaming
HIGH (fix soon):
├── React.cache() for request dedup
├── Parallel component fetching
├── Minimize client serialization
└── SWR for client fetching
MEDIUM (optimize):
├── memo() for expensive components
├── Functional setState
├── startTransition for heavy updates
├── content-visibility for lists
└── Ternary for conditional render
LOW (when time permits):
├── Set/Map for lookups
├── Early returns
├── Combine iterations
└── Cache property access
Checklist
Data Fetching:
├── [ ] No sequential awaits for independent data
├── [ ] Promise.all() used where applicable
├── [ ] React.cache() for server deduplication
├── [ ] SWR for client-side fetching
└── [ ] Suspense boundaries for streaming
Bundle Size:
├── [ ] Direct imports, no barrels
├── [ ] next/dynamic for heavy components
├── [ ] Third-party scripts deferred
├── [ ] Conditional imports for features
└── [ ] Bundle analyzer checked
Rendering:
├── [ ] memo() on expensive components
├── [ ] Functional setState in callbacks
├── [ ] startTransition for heavy updates
├── [ ] content-visibility on long lists
└── [ ] Ternary for conditional rendering
Server:
├── [ ] Minimal data serialization
├── [ ] Parallel component structure
├── [ ] Server actions authenticated
└── [ ] after() for non-blocking ops