Loading...
Loading...
React and Next.js performance optimization patterns adapted from Vercel Engineering's React Best Practices (https://github.com/vercel-labs/agent-skills). Organizes 70+ rules across 8 priority categories — waterfalls, bundle size, server-side, client fetching, re-render, rendering, JS micro-perf, advanced. Use when writing, reviewing, or refactoring React/Next.js code for performance.
npx skill4agent add affaan-m/everything-claude-code react-performancereact-best-practicesapp/pages/components/| Priority | Category | Prefix | When it matters |
|---|---|---|---|
| 1 — CRITICAL | Eliminating Waterfalls | | Anytime |
| 2 — CRITICAL | Bundle Size Optimization | | First-load JS, route-level imports, third-party libs |
| 3 — HIGH | Server-Side Performance | | RSC, Server Actions, API routes, SSR |
| 4 — MEDIUM-HIGH | Client-Side Data Fetching | | SWR / TanStack Query / raw |
| 5 — MEDIUM | Re-render Optimization | | High-frequency state updates, parent-child fan-out |
| 6 — MEDIUM | Rendering Performance | | Long lists, animations, hydration |
| 7 — LOW-MEDIUM | JavaScript Performance | | Hot loops, frequent allocations |
| 8 — LOW | Advanced Patterns | | Effect-event integration, stable refs |
"Waterfalls are the #1 performance killer" — every sequentialadds full network latency.await
// INCORRECT
async function Page({ id }: { id: string }) {
const flag = await getFlag("show-page");
if (!flag || !id) return null;
const data = await getData(id);
// ...
}
// CORRECT — short-circuit on cheap sync condition first
async function Page({ id }: { id: string }) {
if (!id) return null;
const flag = await getFlag("show-page");
if (!flag) return null;
const data = await getData(id);
}await// INCORRECT — awaits before deciding it needs the data
const user = await getUser(id);
if (mode === "guest") return renderGuest();
return renderUser(user);
// CORRECT
if (mode === "guest") return renderGuest();
const user = await getUser(id);
return renderUser(user);// INCORRECT — sequential
const user = await getUser(id);
const posts = await getPosts(id);
const followers = await getFollowers(id);
// CORRECT — parallel
const [user, posts, followers] = await Promise.all([
getUser(id),
getPosts(id),
getFollowers(id),
]);// CORRECT — kick off all promises, await only when each result is needed
const userP = getUser(id);
const postsP = getPosts(id);
const profile = await getProfile(id);
if (profile.private) return null;
const [user, posts] = await Promise.all([userP, postsP]);<Suspense>min-height// INCORRECT — sibling awaits run sequentially inside one component
export default async function Page() {
const user = await getUser();
const cart = await getCart();
return <View user={user} cart={cart} />;
}
// CORRECT — split into children, React runs them in parallel
export default async function Page() {
return (
<View>
<UserSection />
<CartSection />
</View>
);
}index.ts// INCORRECT
import { Button, Card, Modal } from "@/components";
// CORRECT
import { Button } from "@/components/Button";
import { Card } from "@/components/Card";
import { Modal } from "@/components/Modal";// INCORRECT — defeats bundler/trace analysis
const mod = await import(`./pages/${name}`);
// CORRECT — explicit per branch
const mod = name === "home" ? await import("./pages/home") : await import("./pages/about");import dynamic from "next/dynamic";
const HeavyChart = dynamic(() => import("./HeavyChart"), {
loading: () => <Skeleton />,
ssr: false, // when client-only
});next/scriptstrategy="afterInteractive""lazyOnload"if (user.role === "admin") {
const { AdminPanel } = await import("./admin/AdminPanel");
// ...
}<link rel="preload">import()"use server""use server";
export async function deleteUser(formData: FormData) {
const session = await getSession();
if (!session?.user) throw new Error("Unauthorized");
const targetId = String(formData.get("id"));
if (session.user.role !== "admin" && session.user.id !== targetId) {
throw new Error("Forbidden");
}
await db.user.delete({ where: { id: targetId } });
}React.cache()import { cache } from "react";
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});React.cachegetUser("1")unstable_cache// CORRECT — runs once at module load
const fontData = readFileSync(fontPath);
export async function Page() {
return <Banner font={fontData} />;
}headers()cookies()const users = await getUsers();
const enriched = await Promise.all(
users.map(async (u) => ({ ...u, posts: await getPostsFor(u.id) })),
);after()after()import { after } from "next/server";
export async function GET() {
const data = await getData();
after(() => logAnalytics(data));
return Response.json(data);
}useUser(id)useEffectfetch// INCORRECT — every component adds its own
useEffect(() => {
window.addEventListener("scroll", handler);
return () => window.removeEventListener("scroll", handler);
}, []);
// CORRECT — single shared listener via a hook + global subject
const useScroll = createScrollHook(); // singleton subject under the hoodwindow.addEventListener("scroll", handler, { passive: true });preventDefault()versionlocalStorage// INCORRECT — re-renders every time count changes
const count = useStore((s) => s.count);
const handler = () => doSomething(count);
// CORRECT — read once on call
const handler = () => {
const count = useStore.getState().count;
doSomething(count);
};// CORRECT — child re-renders only when `items` changes
const Heavy = memo(function Heavy({ items }: { items: Item[] }) {
return <Chart data={transform(items)} />;
});// INCORRECT — new array each render breaks memo
<List items={items ?? []} />
// CORRECT
const EMPTY: Item[] = [];
<List items={items ?? EMPTY} />// INCORRECT — new object identity every render
useEffect(() => {}, [{ id, name }]);
// CORRECT — primitives
useEffect(() => {}, [id, name]);// INCORRECT — re-renders for any cart change
const cart = useStore((s) => s.cart);
const hasItems = cart.length > 0;
// CORRECT — re-renders only when emptiness flips
const hasItems = useStore((s) => s.cart.length > 0);useEffect// INCORRECT
const [full, setFull] = useState("");
useEffect(() => setFull(`${first} ${last}`), [first, last]);
// CORRECT
const full = `${first} ${last}`;setState// CORRECT
const increment = useCallback(() => setCount((c) => c + 1), []);const [tree] = useState(() => parseTree(largeInput));useMemo(() => x + 1, [x])// INCORRECT — both selectors re-run if either source changes
const { a, b } = useSomething(source1, source2);
// CORRECT
const a = useA(source1);
const b = useB(source2);useEffectstartTransitionconst [pending, startTransition] = useTransition();
startTransition(() => setFilters(newFilters));useDeferredValueconst deferredQuery = useDeferredValue(query);
const results = useMemo(() => expensiveSearch(deferredQuery), [deferredQuery]);useRef// INCORRECT — Inner is a new component on every Outer render
function Outer() {
const Inner = () => <span />;
return <Inner />;
}Inner<div>content-visibility: auto.row { content-visibility: auto; contain-intrinsic-size: auto 80px; }const STATIC_HEADER = <h1>Title</h1>;
function Page() {
return <>{STATIC_HEADER}<Body /></>;
}d="M10.123456,20.654321"d="M10.12,20.65"<script>document.documentElement.dataset.*<time suppressHydrationWarning>{new Date().toLocaleString()}</time><Activity><Activity mode="visible|hidden">&&// INCORRECT — `0` renders as text node
{count && <Badge>{count}</Badge>}
// CORRECT
{count > 0 ? <Badge>{count}</Badge> : null}useTransitionstartTransitionisPendingimport { preload, preconnect } from "react-dom";
preload("/api/critical", { as: "fetch" });
preconnect("https://api.example.com");deferasync<script>deferasynccssTextMapO(1)O(n)const len = arr.lengthMap<key, result>localStoragefilter().map()flatMapforsort()O(n)O(n log n)SetMapO(1)Array.includesO(n)toSorted()flatMaprequestIdleCallbackuseEffectEventuseEffectEventconst handlerRef = useRef(handler);
useEffect(() => { handlerRef.current = handler; });
const stable = useCallback((arg) => handlerRef.current(arg), []);useEffectuseLatestfunction useLatest<T>(value: T) {
const ref = useRef(value);
ref.current = value;
return ref;
}@next/bundle-analyzerrerender-*useMemouseCallback| Metric | Most relevant categories |
|---|---|
| LCP (Largest Contentful Paint) | Waterfalls, Bundle Size, Resource Hints |
| INP (Interaction to Next Paint) | Re-render, Rendering, JavaScript |
| CLS (Cumulative Layout Shift) | Rendering (Suspense placement, image dimensions) |
| TBT (Total Blocking Time) | Bundle Size, JavaScript, Defer Third-Party |
| FID (legacy) | Bundle Size, Hydration |
react-reviewerreact-build-resolver/react-review/react-build/react-testreact-best-practices