UNPKG

opencode-agent-kit

Version:

Multi-stack OpenCode agent toolkit — 33+ specialized AI agents, 200+ skills, 46 commands, 8 MCP servers (Nuxt, React, Node.js, Laravel, CI3, Android, Flutter, DevOps, SEO, SonarQube, and more)

575 lines (408 loc) 18.2 kB
--- name: react-performance description: 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. origin: ECC --- # React Performance Performance optimization patterns for React 18/19 and Next.js, adapted from [Vercel Labs `react-best-practices`](https://github.com/vercel-labs/agent-skills/tree/main/skills/react-best-practices) (MIT, v1.0.0). This skill organizes rules by priority and provides decision-tree guidance for active code review and refactoring. ## When to Activate - Writing or reviewing React/Next.js code for performance - Diagnosing slow page loads, slow interactions, or high CPU on the client - Auditing bundle size or Lighthouse Core Web Vitals regressions - Removing waterfalls in Server Components / API routes - Reducing client-side re-renders - Optimizing long lists, animations, or hydration - Auditing optimization choices in PRs touching `app/`, `pages/`, `components/`, or data layers ## Priority Index | Priority | Category | Prefix | When it matters | |---|---|---|---| | 1 — CRITICAL | Eliminating Waterfalls | `async-` | Anytime `await` is followed by independent `await` | | 2 — CRITICAL | Bundle Size Optimization | `bundle-` | First-load JS, route-level imports, third-party libs | | 3 — HIGH | Server-Side Performance | `server-` | RSC, Server Actions, API routes, SSR | | 4 — MEDIUM-HIGH | Client-Side Data Fetching | `client-` | SWR / TanStack Query / raw `fetch` in hooks | | 5 — MEDIUM | Re-render Optimization | `rerender-` | High-frequency state updates, parent-child fan-out | | 6 — MEDIUM | Rendering Performance | `rendering-` | Long lists, animations, hydration | | 7 — LOW-MEDIUM | JavaScript Performance | `js-` | Hot loops, frequent allocations | | 8 — LOW | Advanced Patterns | `advanced-` | Effect-event integration, stable refs | ## 1. Eliminating Waterfalls (CRITICAL) > "Waterfalls are the #1 performance killer" — every sequential `await` adds full network latency. ### Cheap conditions before await Check sync conditions (props, env, hardcoded flags) before awaiting remote data. ```ts // 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); } ``` ### Defer awaits until used Move `await` into the branch that uses it. ```ts // 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); ``` ### Promise.all for independent work ```ts // 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), ]); ``` ### Partial dependencies — start early, await late ```ts // 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 for streaming Push `<Suspense>` boundaries close to the data so the page paints what it can while slower sub-trees stream in. The trade-off: layout shift when content arrives — reserve space (skeleton or `min-height`). ### Server Components: parallel through composition ```tsx // 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> ); } ``` ## 2. Bundle Size Optimization (CRITICAL) ### Direct imports, not barrels Barrel `index.ts` files force the bundler to walk the entire module graph even when tree-shaking removes most of it. Direct imports save 200-800ms of first-load JS in many real-world apps. ```ts // INCORRECT import { Button, Card, Modal } from "@/components"; // CORRECT import { Button } from "@/components/Button"; import { Card } from "@/components/Card"; import { Modal } from "@/components/Modal"; ``` Next.js 13.5+ has [Optimize Package Imports](https://nextjs.org/docs/app/api-reference/next-config-js/optimizePackageImports) that automates this for listed packages — use it; manual direct imports still required for non-listed libs. ### Statically analyzable paths ```ts // 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"); ``` ### Dynamic imports for heavy components ```tsx import dynamic from "next/dynamic"; const HeavyChart = dynamic(() => import("./HeavyChart"), { loading: () => <Skeleton />, ssr: false, // when client-only }); ``` ### Defer third-party scripts Load analytics, logging, support widgets AFTER hydration. Use `next/script` with `strategy="afterInteractive"` (default) or `"lazyOnload"`. ### Conditional module loading ```tsx if (user.role === "admin") { const { AdminPanel } = await import("./admin/AdminPanel"); // ... } ``` ### Preload on hover/focus Trigger `<link rel="preload">` or `import()` on hover so the bundle is in cache by the time the user clicks. ## 3. Server-Side Performance (HIGH) ### Authenticate Server Actions like API routes Every `"use server"` function is a public endpoint. Authenticate AND authorize inside the action — never rely on the calling Client Component's gating. ```ts "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()` for per-request deduplication ```ts import { cache } from "react"; export const getUser = cache(async (id: string) => { return db.user.findUnique({ where: { id } }); }); ``` `React.cache` dedupes within a single request. Calling `getUser("1")` from three Server Components in the same render = one DB query. ### LRU cache for cross-request data For data that does NOT change per request (config, lookup tables), cache outside React with an LRU cache or `unstable_cache`. ### Avoid duplicate serialization in RSC props When a Server Component renders the same data into multiple Client Components, the data is serialized once per consumer. Lift the Client Component up and pass children. ### Hoist static I/O to module scope ```ts // CORRECT — runs once at module load const fontData = readFileSync(fontPath); export async function Page() { return <Banner font={fontData} />; } ``` ### No mutable module-level state in RSC/SSR Module state on the server is shared across all requests — a race condition between users. Use request-scoped storage (`headers()`, `cookies()`, async context) instead. ### Minimize data passed to Client Components Only serialize what the Client needs. Strip fields, paginate, project columns at the DB layer. ### Parallelize nested fetches with Promise.all per item ```ts const users = await getUsers(); const enriched = await Promise.all( users.map(async (u) => ({ ...u, posts: await getPostsFor(u.id) })), ); ``` ### Use `after()` for non-blocking work Next.js 15 `after()` runs work after the response is sent — logging, cache warming, analytics. ```ts import { after } from "next/server"; export async function GET() { const data = await getData(); after(() => logAnalytics(data)); return Response.json(data); } ``` ## 4. Client-Side Data Fetching (MEDIUM-HIGH) ### SWR / TanStack Query for deduplication Multiple components calling `useUser(id)` should share one network request and one cache entry. Use SWR or TanStack Query — never roll your own `useEffect` + `fetch` for shared data. ### Deduplicate global event listeners ```tsx // 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 hood ``` ### Passive listeners for scroll ```ts window.addEventListener("scroll", handler, { passive: true }); ``` Improves scrolling smoothness; the listener cannot `preventDefault()`. ### localStorage: version + minimize - Always store a `version` field; bump on schema change and migrate or discard old data - Keep payloads small — `localStorage` is synchronous and blocks main thread ## 5. Re-render Optimization (MEDIUM) ### Don't subscribe to state used only in callbacks ```tsx // 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); }; ``` ### Extract expensive work into memoized components ```tsx // CORRECT — child re-renders only when `items` changes const Heavy = memo(function Heavy({ items }: { items: Item[] }) { return <Chart data={transform(items)} />; }); ``` ### Hoist default non-primitive props ```tsx // INCORRECT — new array each render breaks memo <List items={items ?? []} /> // CORRECT const EMPTY: Item[] = []; <List items={items ?? EMPTY} /> ``` ### Primitive dependencies in effects ```tsx // INCORRECT — new object identity every render useEffect(() => {}, [{ id, name }]); // CORRECT — primitives useEffect(() => {}, [id, name]); ``` ### Subscribe to derived booleans, not raw values ```tsx // 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); ``` ### Derive during render, never via `useEffect` ```tsx // INCORRECT const [full, setFull] = useState(""); useEffect(() => setFull(`${first} ${last}`), [first, last]); // CORRECT const full = `${first} ${last}`; ``` ### Functional `setState` for stable callbacks ```tsx // CORRECT const increment = useCallback(() => setCount((c) => c + 1), []); ``` ### Lazy state initializer for expensive values ```tsx const [tree] = useState(() => parseTree(largeInput)); ``` ### Avoid memo for simple primitives `useMemo(() => x + 1, [x])` is overhead. Memo earns its keep on object identity and expensive computation. ### Split hooks with independent deps ```tsx // INCORRECT — both selectors re-run if either source changes const { a, b } = useSomething(source1, source2); // CORRECT const a = useA(source1); const b = useB(source2); ``` ### Move interaction logic into event handlers Event handlers run only on the user action — `useEffect` re-runs whenever deps change. ### `startTransition` for non-urgent updates ```tsx const [pending, startTransition] = useTransition(); startTransition(() => setFilters(newFilters)); ``` ### `useDeferredValue` for expensive renders ```tsx const deferredQuery = useDeferredValue(query); const results = useMemo(() => expensiveSearch(deferredQuery), [deferredQuery]); ``` ### `useRef` for transient frequent values For values that change often but should not trigger re-render (timestamps, last-key, accumulators). ### Don't define components inside components ```tsx // INCORRECT — Inner is a new component on every Outer render function Outer() { const Inner = () => <span />; return <Inner />; } ``` Each render makes a new `Inner` type, defeating reconciliation and unmounting children. ## 6. Rendering Performance (MEDIUM) ### Animate the wrapper, not the SVG Transforming a `<div>` wrapper around an SVG is GPU-accelerated; transforming the SVG itself triggers paint. ### `content-visibility: auto` for long lists ```css .row { content-visibility: auto; contain-intrinsic-size: auto 80px; } ``` Browser skips offscreen rendering — major win for lists with hundreds of rows. ### Hoist static JSX ```tsx const STATIC_HEADER = <h1>Title</h1>; function Page() { return <>{STATIC_HEADER}<Body /></>; } ``` ### SVG: reduce coordinate precision `d="M10.123456,20.654321"``d="M10.12,20.65"`. Each digit costs bytes; the visual difference is sub-pixel. ### Hydration no-flicker via inline script For values needed before hydration (theme, locale), inline a `<script>` that sets `document.documentElement.dataset.*` before React mounts. ### Suppress expected hydration mismatches narrowly ```tsx <time suppressHydrationWarning>{new Date().toLocaleString()}</time> ``` Use ONLY for known-divergent leaf nodes — never on a tree containing other children. ### `<Activity>` for show/hide instead of mount/unmount React 19 `<Activity mode="visible|hidden">` keeps tree state and effects mounted but hides — cheaper than unmount/remount for tabs and accordions. ### Ternary over `&&` for conditional render ```tsx // INCORRECT — `0` renders as text node {count && <Badge>{count}</Badge>} // CORRECT {count > 0 ? <Badge>{count}</Badge> : null} ``` ### `useTransition` for loading states Pair `startTransition` with the action; React shows the previous UI as `isPending` while the next state computes. ### React DOM resource hints ```tsx import { preload, preconnect } from "react-dom"; preload("/api/critical", { as: "fetch" }); preconnect("https://api.example.com"); ``` ### `defer` / `async` on `<script>` tags `defer` for ordered execution after DOMContentLoaded; `async` for fire-and-forget. ## 7. JavaScript Performance (LOW-MEDIUM) - **Batch DOM/CSS changes** — apply via class swap or `cssText`, not property-by-property - **`Map` for repeated lookups**`O(1)` vs `O(n)` linear scan - **Cache property access in loops**`const len = arr.length` - **Memoize pure functions** — module-level `Map<key, result>` - **Cache `localStorage` reads** — sync API; one read per render - **Combine `filter().map()` into one pass**`flatMap` or single `for` - **Check array length first** before expensive comparisons - **Early return** from functions - **Hoist RegExp** out of loops — compilation is not free - **Loop for min/max** instead of `sort()``O(n)` vs `O(n log n)` - **`Set`/`Map` for membership**`O(1)` vs `Array.includes` `O(n)` - **`toSorted()` over mutation** when immutability matters - **`flatMap` to map and filter in one pass** - **`requestIdleCallback`** for non-critical work ## 8. Advanced Patterns (LOW) ### `useEffectEvent` deps Values from `useEffectEvent` are stable — do NOT add them to effect deps. ### Event handler refs For stable callbacks passed to memoized children: ```tsx const handlerRef = useRef(handler); useEffect(() => { handlerRef.current = handler; }); const stable = useCallback((arg) => handlerRef.current(arg), []); ``` ### Init once per app load For module-level singletons (telemetry, logger), guard with a module-scope flag — not `useEffect`. ### `useLatest` for stable callback refs ```tsx function useLatest<T>(value: T) { const ref = useRef(value); ref.current = value; return ref; } ``` ## Automated Tools Many of these rules are now automated: - **Next.js 13.5+ Optimize Package Imports** — barrel import optimization - **React Compiler** (RFC, in canary) — auto-memoization - **Turbopack** — faster builds, better tree-shaking - **Bundle Analyzer** (`@next/bundle-analyzer`) — visualize first-load JS When the project ships React Compiler, demote `rerender-*` manual memoization rules to "review-only" — the compiler handles them. Manual `useMemo`/`useCallback` becomes unnecessary noise. ## Lighthouse / Web Vitals Mapping | 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 | ## Related - Skills: [react-patterns](../react-patterns/SKILL.md), [react-testing](../react-testing/SKILL.md), [frontend-patterns](../frontend-patterns/SKILL.md), [accessibility](../accessibility/SKILL.md), [nextjs-turbopack](../nextjs-turbopack/SKILL.md) - Rules: [rules/react/](../../rules/react/) - Agents: `react-reviewer` enforces these rules in code review; `react-build-resolver` handles related build failures - Commands: `/react-review`, `/react-build`, `/react-test` ## Attribution Adapted from Vercel Labs `react-best-practices` skill (MIT License, copyright Vercel Engineering, v1.0.0 January 2026). Source: [https://github.com/vercel-labs/agent-skills/tree/main/skills/react-best-practices](https://github.com/vercel-labs/agent-skills/tree/main/skills/react-best-practices). This skill restructures and adapts the original 70-rule catalog into a single navigable reference. For the full original ruleset with extended examples, see the upstream repository.