@codihaus/claude-skills 1.6.18 → 1.6.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/stacks/_index.md +1 -0
- package/knowledge/stacks/nextjs/_index.md +32 -0
- package/knowledge/stacks/nextjs/references/rsc-patterns.md +705 -0
- package/knowledge/stacks/react/_index.md +21 -0
- package/knowledge/stacks/react/references/performance.md +573 -0
- package/knowledge/stacks/vue/_index.md +750 -0
- package/package.json +1 -1
- package/skills/_registry.md +1 -0
- package/skills/dev-coding/SKILL.md +4 -1
- package/skills/dev-review/SKILL.md +11 -1
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
# Next.js RSC & Server Actions Patterns
|
|
2
|
+
|
|
3
|
+
> Critical patterns for React Server Components and Server Actions from Vercel Engineering
|
|
4
|
+
|
|
5
|
+
## Eliminating Waterfalls
|
|
6
|
+
|
|
7
|
+
### Defer Await Until Needed
|
|
8
|
+
|
|
9
|
+
**Impact**: HIGH - Avoids blocking unused code paths
|
|
10
|
+
|
|
11
|
+
Move `await` operations into branches where they're actually used:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// ❌ Incorrect: blocks both branches
|
|
15
|
+
async function handleRequest(userId: string, skipProcessing: boolean) {
|
|
16
|
+
const userData = await fetchUserData(userId)
|
|
17
|
+
|
|
18
|
+
if (skipProcessing) {
|
|
19
|
+
return { skipped: true } // Still waited!
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return processUserData(userData)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ✅ Correct: only blocks when needed
|
|
26
|
+
async function handleRequest(userId: string, skipProcessing: boolean) {
|
|
27
|
+
if (skipProcessing) {
|
|
28
|
+
return { skipped: true } // No wait
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const userData = await fetchUserData(userId)
|
|
32
|
+
return processUserData(userData)
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Early return optimization**:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// ❌ Incorrect: always fetches permissions
|
|
40
|
+
async function updateResource(resourceId: string, userId: string) {
|
|
41
|
+
const permissions = await fetchPermissions(userId)
|
|
42
|
+
const resource = await getResource(resourceId)
|
|
43
|
+
|
|
44
|
+
if (!resource) return { error: 'Not found' }
|
|
45
|
+
if (!permissions.canEdit) return { error: 'Forbidden' }
|
|
46
|
+
|
|
47
|
+
return await updateResourceData(resource, permissions)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ✅ Correct: check resource first (cheaper)
|
|
51
|
+
async function updateResource(resourceId: string, userId: string) {
|
|
52
|
+
const resource = await getResource(resourceId)
|
|
53
|
+
if (!resource) return { error: 'Not found' }
|
|
54
|
+
|
|
55
|
+
const permissions = await fetchPermissions(userId)
|
|
56
|
+
if (!permissions.canEdit) return { error: 'Forbidden' }
|
|
57
|
+
|
|
58
|
+
return await updateResourceData(resource, permissions)
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Dependency-Based Parallelization
|
|
63
|
+
|
|
64
|
+
**Impact**: CRITICAL - 2-10× improvement
|
|
65
|
+
|
|
66
|
+
Use `better-all` for partial dependencies:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// ❌ Incorrect: profile waits for config unnecessarily
|
|
70
|
+
const [user, config] = await Promise.all([
|
|
71
|
+
fetchUser(),
|
|
72
|
+
fetchConfig()
|
|
73
|
+
])
|
|
74
|
+
const profile = await fetchProfile(user.id)
|
|
75
|
+
|
|
76
|
+
// ✅ Correct: config and profile run in parallel
|
|
77
|
+
import { all } from 'better-all'
|
|
78
|
+
|
|
79
|
+
const { user, config, profile } = await all({
|
|
80
|
+
async user() { return fetchUser() },
|
|
81
|
+
async config() { return fetchConfig() },
|
|
82
|
+
async profile() {
|
|
83
|
+
return fetchProfile((await this.$.user).id)
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Prevent Waterfall Chains in API Routes
|
|
89
|
+
|
|
90
|
+
**Impact**: CRITICAL - 2-10× improvement
|
|
91
|
+
|
|
92
|
+
Start independent operations immediately:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
// ❌ Incorrect: sequential
|
|
96
|
+
export async function GET(request: Request) {
|
|
97
|
+
const session = await auth()
|
|
98
|
+
const config = await fetchConfig()
|
|
99
|
+
const data = await fetchData(session.user.id)
|
|
100
|
+
return Response.json({ data, config })
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ✅ Correct: parallel where possible
|
|
104
|
+
export async function GET(request: Request) {
|
|
105
|
+
const sessionPromise = auth()
|
|
106
|
+
const configPromise = fetchConfig()
|
|
107
|
+
|
|
108
|
+
const session = await sessionPromise
|
|
109
|
+
const [config, data] = await Promise.all([
|
|
110
|
+
configPromise,
|
|
111
|
+
fetchData(session.user.id)
|
|
112
|
+
])
|
|
113
|
+
|
|
114
|
+
return Response.json({ data, config })
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Promise.all() for Independent Operations
|
|
119
|
+
|
|
120
|
+
**Impact**: CRITICAL - Reduces N round trips to 1
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
// ❌ Incorrect: sequential (3 round trips)
|
|
124
|
+
const user = await fetchUser()
|
|
125
|
+
const posts = await fetchPosts()
|
|
126
|
+
const comments = await fetchComments()
|
|
127
|
+
|
|
128
|
+
// ✅ Correct: parallel (1 round trip)
|
|
129
|
+
const [user, posts, comments] = await Promise.all([
|
|
130
|
+
fetchUser(),
|
|
131
|
+
fetchPosts(),
|
|
132
|
+
fetchComments()
|
|
133
|
+
])
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Strategic Suspense Boundaries
|
|
137
|
+
|
|
138
|
+
**Impact**: Faster initial paint
|
|
139
|
+
|
|
140
|
+
Show wrapper UI immediately while data streams in:
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
// ❌ Incorrect: all or nothing
|
|
144
|
+
export default async function Page() {
|
|
145
|
+
const [header, content, sidebar] = await Promise.all([
|
|
146
|
+
fetchHeader(),
|
|
147
|
+
fetchContent(),
|
|
148
|
+
fetchSidebar()
|
|
149
|
+
])
|
|
150
|
+
return <Layout header={header} content={content} sidebar={sidebar} />
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ✅ Correct: streams in progressively
|
|
154
|
+
async function Header() {
|
|
155
|
+
const data = await fetchHeader()
|
|
156
|
+
return <header>{data}</header>
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function Content() {
|
|
160
|
+
const data = await fetchContent()
|
|
161
|
+
return <main>{data}</main>
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function Sidebar() {
|
|
165
|
+
const data = await fetchSidebar()
|
|
166
|
+
return <aside>{data}</aside>
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export default function Page() {
|
|
170
|
+
return (
|
|
171
|
+
<>
|
|
172
|
+
<Suspense fallback={<HeaderSkeleton />}>
|
|
173
|
+
<Header />
|
|
174
|
+
</Suspense>
|
|
175
|
+
<Suspense fallback={<ContentSkeleton />}>
|
|
176
|
+
<Content />
|
|
177
|
+
</Suspense>
|
|
178
|
+
<Suspense fallback={<SidebarSkeleton />}>
|
|
179
|
+
<Sidebar />
|
|
180
|
+
</Suspense>
|
|
181
|
+
</>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Share promises with `use()` hook**:
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
const dataPromise = fetchData()
|
|
190
|
+
|
|
191
|
+
function ParentComponent() {
|
|
192
|
+
return (
|
|
193
|
+
<>
|
|
194
|
+
<ChildA dataPromise={dataPromise} />
|
|
195
|
+
<ChildB dataPromise={dataPromise} />
|
|
196
|
+
</>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function ChildA({ dataPromise }) {
|
|
201
|
+
const data = use(dataPromise) // Shares same promise
|
|
202
|
+
return <div>{data.title}</div>
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function ChildB({ dataPromise }) {
|
|
206
|
+
const data = use(dataPromise) // Same promise, no duplicate fetch
|
|
207
|
+
return <div>{data.count}</div>
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Server-Side Performance
|
|
212
|
+
|
|
213
|
+
### Cross-Request LRU Caching
|
|
214
|
+
|
|
215
|
+
**Impact**: HIGH - Especially with Vercel Fluid Compute
|
|
216
|
+
|
|
217
|
+
`React.cache()` only works within one request. Use LRU cache for sequential user actions:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { LRUCache } from 'lru-cache'
|
|
221
|
+
|
|
222
|
+
const userCache = new LRUCache<string, User>({
|
|
223
|
+
max: 1000,
|
|
224
|
+
ttl: 5 * 60 * 1000 // 5 minutes
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
export async function getUser(id: string) {
|
|
228
|
+
const cached = userCache.get(id)
|
|
229
|
+
if (cached) return cached
|
|
230
|
+
|
|
231
|
+
const user = await db.user.findUnique({ where: { id } })
|
|
232
|
+
userCache.set(id, user)
|
|
233
|
+
return user
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Why this matters**: With Vercel Fluid Compute, the same instance handles multiple requests from the same user. LRU cache survives across requests.
|
|
238
|
+
|
|
239
|
+
### Minimize Serialization at RSC Boundaries
|
|
240
|
+
|
|
241
|
+
**Impact**: HIGH - Reduces data transfer size
|
|
242
|
+
|
|
243
|
+
Only pass fields the client actually uses:
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// ❌ Incorrect: serializes all 50 fields
|
|
247
|
+
async function Page() {
|
|
248
|
+
const user = await fetchUser() // 50 fields
|
|
249
|
+
return <Profile user={user} /> // Client uses 1 field
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ✅ Correct: serializes only 1 field
|
|
253
|
+
async function Page() {
|
|
254
|
+
const user = await fetchUser()
|
|
255
|
+
return <Profile name={user.name} />
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ✅ Alternative: transform before passing
|
|
259
|
+
async function Page() {
|
|
260
|
+
const user = await fetchUser()
|
|
261
|
+
const clientData = {
|
|
262
|
+
name: user.name,
|
|
263
|
+
avatar: user.avatar
|
|
264
|
+
}
|
|
265
|
+
return <Profile user={clientData} />
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Why this matters**: Data is embedded in HTML response AND RSC requests. Large objects increase payload size and parsing time.
|
|
270
|
+
|
|
271
|
+
### Parallel Data Fetching with Component Composition
|
|
272
|
+
|
|
273
|
+
**Impact**: CRITICAL - Eliminates server-side waterfalls
|
|
274
|
+
|
|
275
|
+
RSCs execute sequentially within a tree. Restructure to parallelize:
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
// ❌ Incorrect: Sidebar waits for Page's fetch
|
|
279
|
+
export default async function Page() {
|
|
280
|
+
const header = await fetchHeader()
|
|
281
|
+
return (
|
|
282
|
+
<div>
|
|
283
|
+
<div>{header}</div>
|
|
284
|
+
<Sidebar /> {/* Waits for header */}
|
|
285
|
+
</div>
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ✅ Correct: both fetch simultaneously
|
|
290
|
+
async function Header() {
|
|
291
|
+
const data = await fetchHeader()
|
|
292
|
+
return <div>{data}</div>
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export default function Page() {
|
|
296
|
+
return (
|
|
297
|
+
<div>
|
|
298
|
+
<Header />
|
|
299
|
+
<Sidebar /> {/* Parallel! */}
|
|
300
|
+
</div>
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**Pro tip**: Move async calls into separate components at the same level to maximize parallelism.
|
|
306
|
+
|
|
307
|
+
### Per-Request Deduplication with React.cache()
|
|
308
|
+
|
|
309
|
+
**Impact**: HIGH - Prevents duplicate queries in one render
|
|
310
|
+
|
|
311
|
+
**Important**: In Next.js, `fetch` is auto-deduplicated. Use `React.cache()` for:
|
|
312
|
+
- Database queries
|
|
313
|
+
- Heavy computations
|
|
314
|
+
- Auth checks
|
|
315
|
+
- File system operations
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
import { cache } from 'react'
|
|
319
|
+
|
|
320
|
+
// ✅ Correct: multiple calls = one execution
|
|
321
|
+
export const getUser = cache(async (id: string) => {
|
|
322
|
+
return await db.user.findUnique({ where: { id } })
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
// Usage in multiple components
|
|
326
|
+
async function Profile() {
|
|
327
|
+
const user = await getUser('123') // Executes
|
|
328
|
+
return <div>{user.name}</div>
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function Avatar() {
|
|
332
|
+
const user = await getUser('123') // Cached!
|
|
333
|
+
return <img src={user.avatar} />
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
**Warning**: Avoid inline objects as arguments (always cache miss):
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
// ❌ Incorrect: cache miss every time (different object reference)
|
|
341
|
+
const getData = cache(async (options: { id: string }) => {
|
|
342
|
+
return await db.query(options.id)
|
|
343
|
+
})
|
|
344
|
+
getData({ id: '123' }) // Miss
|
|
345
|
+
getData({ id: '123' }) // Miss (different object!)
|
|
346
|
+
|
|
347
|
+
// ✅ Correct: use primitives or stable references
|
|
348
|
+
const getData = cache(async (id: string) => {
|
|
349
|
+
return await db.query(id)
|
|
350
|
+
})
|
|
351
|
+
getData('123') // Hit
|
|
352
|
+
getData('123') // Hit
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Use after() for Non-Blocking Operations
|
|
356
|
+
|
|
357
|
+
**Impact**: HIGH - Faster response times
|
|
358
|
+
|
|
359
|
+
Schedule work after response is sent:
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
import { after } from 'next/server'
|
|
363
|
+
|
|
364
|
+
export async function POST(request: Request) {
|
|
365
|
+
const data = await request.json()
|
|
366
|
+
await updateDatabase(data)
|
|
367
|
+
|
|
368
|
+
after(async () => {
|
|
369
|
+
// These don't block the response
|
|
370
|
+
await logUserAction(data)
|
|
371
|
+
await sendAnalytics(data)
|
|
372
|
+
await invalidateCache(data.id)
|
|
373
|
+
await sendNotification(data.userId)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
return new Response(JSON.stringify({ status: 'success' }))
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
**Perfect for**:
|
|
381
|
+
- Analytics
|
|
382
|
+
- Audit logging
|
|
383
|
+
- Notifications
|
|
384
|
+
- Cache invalidation
|
|
385
|
+
- External webhooks
|
|
386
|
+
|
|
387
|
+
## Bundle Size Optimization
|
|
388
|
+
|
|
389
|
+
### Avoid Barrel File Imports
|
|
390
|
+
|
|
391
|
+
**Impact**: CRITICAL - 200-800ms import cost, 15-70% faster builds
|
|
392
|
+
|
|
393
|
+
Popular libraries have 1000s of re-exports. Tree-shaking doesn't help:
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
// ❌ Incorrect: loads 1,583 modules, ~2.8s in dev
|
|
397
|
+
import { Check, X, Menu } from 'lucide-react'
|
|
398
|
+
|
|
399
|
+
// ✅ Correct: loads only 3 modules
|
|
400
|
+
import Check from 'lucide-react/dist/esm/icons/check'
|
|
401
|
+
import X from 'lucide-react/dist/esm/icons/x'
|
|
402
|
+
import Menu from 'lucide-react/dist/esm/icons/menu'
|
|
403
|
+
|
|
404
|
+
// ✅ Best: Next.js 13.5+ (auto-optimized)
|
|
405
|
+
// next.config.js
|
|
406
|
+
module.exports = {
|
|
407
|
+
experimental: {
|
|
408
|
+
optimizePackageImports: ['lucide-react', '@mui/material', '@mui/icons-material']
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Then use barrel imports normally
|
|
412
|
+
import { Check, X, Menu } from 'lucide-react' // Optimized!
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
**Affected libraries**: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`
|
|
416
|
+
|
|
417
|
+
### Conditional Module Loading
|
|
418
|
+
|
|
419
|
+
**Impact**: HIGH - Reduces initial bundle
|
|
420
|
+
|
|
421
|
+
Load large modules only when feature is activated:
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
// ❌ Incorrect: always bundled
|
|
425
|
+
import { processData } from './heavy-library'
|
|
426
|
+
|
|
427
|
+
export default function Component() {
|
|
428
|
+
const [advanced, setAdvanced] = useState(false)
|
|
429
|
+
|
|
430
|
+
if (!advanced) return <SimpleView />
|
|
431
|
+
|
|
432
|
+
return <AdvancedView data={processData(input)} />
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ✅ Correct: loaded only when needed
|
|
436
|
+
export default function Component() {
|
|
437
|
+
const [advanced, setAdvanced] = useState(false)
|
|
438
|
+
|
|
439
|
+
if (!advanced) return <SimpleView />
|
|
440
|
+
|
|
441
|
+
const { processData } = await import('./heavy-library')
|
|
442
|
+
return <AdvancedView data={processData(input)} />
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
**Client-side SSR prevention**:
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
// Prevents bundling in server bundle
|
|
450
|
+
if (typeof window !== 'undefined') {
|
|
451
|
+
const { heavyClientLib } = await import('./client-only')
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Defer Non-Critical Third-Party Libraries
|
|
456
|
+
|
|
457
|
+
**Impact**: HIGH - Analytics/monitoring don't block interaction
|
|
458
|
+
|
|
459
|
+
Load after hydration:
|
|
460
|
+
|
|
461
|
+
```tsx
|
|
462
|
+
// ❌ Incorrect: blocks hydration
|
|
463
|
+
import Analytics from 'analytics-lib'
|
|
464
|
+
|
|
465
|
+
export default function Layout({ children }) {
|
|
466
|
+
return (
|
|
467
|
+
<>
|
|
468
|
+
<Analytics />
|
|
469
|
+
{children}
|
|
470
|
+
</>
|
|
471
|
+
)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ✅ Correct: loads after hydration
|
|
475
|
+
const Analytics = dynamic(() => import('./Analytics'), {
|
|
476
|
+
ssr: false // Client-only
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
export default function Layout({ children }) {
|
|
480
|
+
return (
|
|
481
|
+
<>
|
|
482
|
+
{children}
|
|
483
|
+
<Analytics />
|
|
484
|
+
</>
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### Dynamic Imports for Heavy Components
|
|
490
|
+
|
|
491
|
+
**Impact**: CRITICAL - Directly affects TTI and LCP
|
|
492
|
+
|
|
493
|
+
Example: Monaco Editor (~300KB) should always be dynamic:
|
|
494
|
+
|
|
495
|
+
```tsx
|
|
496
|
+
// ❌ Incorrect: adds 300KB to initial bundle
|
|
497
|
+
import Editor from '@monaco-editor/react'
|
|
498
|
+
|
|
499
|
+
export default function Page() {
|
|
500
|
+
return <Editor />
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ✅ Correct: loaded on demand
|
|
504
|
+
const Editor = dynamic(() => import('@monaco-editor/react'), {
|
|
505
|
+
loading: () => <div>Loading editor...</div>,
|
|
506
|
+
ssr: false
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
export default function Page() {
|
|
510
|
+
return <Editor />
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Preload Based on User Intent
|
|
515
|
+
|
|
516
|
+
**Impact**: MEDIUM-HIGH - Feels instant
|
|
517
|
+
|
|
518
|
+
Preload before click using hover/focus:
|
|
519
|
+
|
|
520
|
+
```tsx
|
|
521
|
+
import { useState } from 'react'
|
|
522
|
+
|
|
523
|
+
function NavigationLink({ href, children }) {
|
|
524
|
+
const [isPreloading, setIsPreloading] = useState(false)
|
|
525
|
+
|
|
526
|
+
return (
|
|
527
|
+
<Link
|
|
528
|
+
href={href}
|
|
529
|
+
onMouseEnter={() => {
|
|
530
|
+
if (!isPreloading) {
|
|
531
|
+
setIsPreloading(true)
|
|
532
|
+
// Preload route
|
|
533
|
+
router.prefetch(href)
|
|
534
|
+
}
|
|
535
|
+
}}
|
|
536
|
+
onFocus={() => {
|
|
537
|
+
if (!isPreloading) {
|
|
538
|
+
setIsPreloading(true)
|
|
539
|
+
router.prefetch(href)
|
|
540
|
+
}
|
|
541
|
+
}}
|
|
542
|
+
>
|
|
543
|
+
{children}
|
|
544
|
+
</Link>
|
|
545
|
+
)
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
## Data Fetching Strategies
|
|
550
|
+
|
|
551
|
+
### fetch is Auto-Deduplicated
|
|
552
|
+
|
|
553
|
+
**Built-in**: Same URL + options = one request per render
|
|
554
|
+
|
|
555
|
+
```tsx
|
|
556
|
+
// ✅ Automatic deduplication
|
|
557
|
+
async function Header() {
|
|
558
|
+
const data = await fetch('/api/data') // Request 1
|
|
559
|
+
return <div>{data.title}</div>
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function Content() {
|
|
563
|
+
const data = await fetch('/api/data') // Deduplicated!
|
|
564
|
+
return <div>{data.body}</div>
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### React.cache() for Non-Fetch
|
|
569
|
+
|
|
570
|
+
Use for DB queries, file ops, computations:
|
|
571
|
+
|
|
572
|
+
```typescript
|
|
573
|
+
import { cache } from 'react'
|
|
574
|
+
|
|
575
|
+
export const getSettings = cache(async () => {
|
|
576
|
+
// Heavy computation or DB query
|
|
577
|
+
return await db.settings.findFirst()
|
|
578
|
+
})
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### Client-Side: Use SWR for Deduplication
|
|
582
|
+
|
|
583
|
+
```typescript
|
|
584
|
+
import useSWR from 'swr'
|
|
585
|
+
|
|
586
|
+
// Multiple instances share one request
|
|
587
|
+
function Component() {
|
|
588
|
+
const { data } = useSWR('/api/user', fetcher)
|
|
589
|
+
return <div>{data?.name}</div>
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Use useImmutableSWR for static data
|
|
593
|
+
import { useImmutableSWR } from 'swr/immutable'
|
|
594
|
+
|
|
595
|
+
function StaticComponent() {
|
|
596
|
+
const { data } = useImmutableSWR('/api/config', fetcher)
|
|
597
|
+
return <div>{data?.appName}</div>
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
## Server Actions Best Practices
|
|
602
|
+
|
|
603
|
+
### Prevent Waterfalls
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
'use server'
|
|
607
|
+
|
|
608
|
+
// ❌ Incorrect: sequential
|
|
609
|
+
export async function updateProfile(data: FormData) {
|
|
610
|
+
const user = await getUser()
|
|
611
|
+
const validated = await validateData(data)
|
|
612
|
+
const result = await saveToDb(validated)
|
|
613
|
+
return result
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ✅ Correct: parallel where possible
|
|
617
|
+
export async function updateProfile(data: FormData) {
|
|
618
|
+
const userPromise = getUser()
|
|
619
|
+
const validatedPromise = validateData(data)
|
|
620
|
+
|
|
621
|
+
const [user, validated] = await Promise.all([
|
|
622
|
+
userPromise,
|
|
623
|
+
validatedPromise
|
|
624
|
+
])
|
|
625
|
+
|
|
626
|
+
const result = await saveToDb(validated)
|
|
627
|
+
return result
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### Use after() for Non-Blocking Work
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
'use server'
|
|
635
|
+
|
|
636
|
+
import { after } from 'next/server'
|
|
637
|
+
|
|
638
|
+
export async function createPost(data: FormData) {
|
|
639
|
+
const post = await db.post.create({ data })
|
|
640
|
+
|
|
641
|
+
after(async () => {
|
|
642
|
+
await revalidatePath('/blog')
|
|
643
|
+
await sendNotification(post.authorId)
|
|
644
|
+
await trackEvent('post_created', { postId: post.id })
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
return { success: true, postId: post.id }
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### React.cache() for Shared Logic
|
|
652
|
+
|
|
653
|
+
```typescript
|
|
654
|
+
import { cache } from 'react'
|
|
655
|
+
|
|
656
|
+
export const getCurrentUser = cache(async () => {
|
|
657
|
+
const session = await auth()
|
|
658
|
+
if (!session) return null
|
|
659
|
+
return await db.user.findUnique({ where: { id: session.userId } })
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
// Multiple server actions can call this
|
|
663
|
+
export async function updateProfile(data: FormData) {
|
|
664
|
+
const user = await getCurrentUser() // Cached
|
|
665
|
+
// ...
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export async function deleteAccount() {
|
|
669
|
+
const user = await getCurrentUser() // Same cache
|
|
670
|
+
// ...
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
## Anti-Patterns to Avoid
|
|
675
|
+
|
|
676
|
+
### Critical Anti-Patterns
|
|
677
|
+
1. ❌ Sequential awaits for independent operations
|
|
678
|
+
2. ❌ Barrel file imports (200-800ms overhead)
|
|
679
|
+
3. ❌ Awaiting in parent RSC before rendering children
|
|
680
|
+
4. ❌ Passing entire objects across RSC boundary
|
|
681
|
+
|
|
682
|
+
### High-Impact Anti-Patterns
|
|
683
|
+
5. ❌ No deduplication for repeated calls (use React.cache/SWR)
|
|
684
|
+
6. ❌ Blocking responses with logging/analytics (use after())
|
|
685
|
+
7. ❌ Inline objects as React.cache() arguments
|
|
686
|
+
8. ❌ Not using optimizePackageImports for icon libraries
|
|
687
|
+
|
|
688
|
+
### Subtle Bugs
|
|
689
|
+
9. ❌ Forgetting `ssr: false` for client-only libraries
|
|
690
|
+
10. ❌ Not hoisting async calls to enable parallelization
|
|
691
|
+
11. ❌ Using barrel imports in dev (slow HMR)
|
|
692
|
+
12. ❌ LRU cache without TTL (memory leak)
|
|
693
|
+
|
|
694
|
+
## Key Metrics
|
|
695
|
+
|
|
696
|
+
| Optimization | Improvement |
|
|
697
|
+
|---|---|
|
|
698
|
+
| Eliminate waterfalls | 2-10× faster |
|
|
699
|
+
| Avoid barrel imports | 15-70% faster builds, 28% faster HMR |
|
|
700
|
+
| Bundle size reduction | Direct TTI/LCP impact |
|
|
701
|
+
| LRU caching | Eliminates redundant DB queries |
|
|
702
|
+
| Parallel RSC composition | Eliminates server waterfalls |
|
|
703
|
+
| after() for non-blocking | 50-200ms faster responses |
|
|
704
|
+
|
|
705
|
+
**Reference**: [Vercel React Best Practices](https://github.com/vercel/react-best-practices)
|