@codihaus/claude-skills 1.6.19 → 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.
|
@@ -637,6 +637,31 @@ if (process.env.NODE_ENV !== 'production') {
|
|
|
637
637
|
}
|
|
638
638
|
```
|
|
639
639
|
|
|
640
|
+
## Advanced RSC & Performance Patterns
|
|
641
|
+
|
|
642
|
+
**See `references/rsc-patterns.md` for comprehensive Next.js optimization guide covering**:
|
|
643
|
+
- Eliminating waterfalls (defer await, dependency parallelization, Promise.all)
|
|
644
|
+
- Server-side performance (LRU caching, serialization, React.cache, after())
|
|
645
|
+
- Bundle optimization (avoid barrel imports, dynamic imports, preloading)
|
|
646
|
+
- Data fetching strategies (auto-deduplication, SWR, caching)
|
|
647
|
+
- Server Actions best practices
|
|
648
|
+
|
|
649
|
+
**Critical patterns**:
|
|
650
|
+
- **Defer await until needed** - Don't block unused code paths
|
|
651
|
+
- **Start promises early** - Enable parallelization in API routes
|
|
652
|
+
- **Component composition** - Move async calls to components for parallel execution
|
|
653
|
+
- **Minimize serialization** - Only pass needed fields across RSC boundary
|
|
654
|
+
- **React.cache()** - Deduplicate DB queries, auth checks, heavy computations
|
|
655
|
+
- **after()** - Schedule non-blocking work (analytics, logging)
|
|
656
|
+
- **Avoid barrel imports** - Use optimizePackageImports or direct imports
|
|
657
|
+
- **LRU caching** - Share data across requests (especially with Fluid Compute)
|
|
658
|
+
|
|
659
|
+
**Impact metrics**:
|
|
660
|
+
- Eliminating waterfalls: 2-10× faster
|
|
661
|
+
- Avoiding barrel imports: 15-70% faster builds
|
|
662
|
+
- LRU caching: Eliminates redundant queries
|
|
663
|
+
- after() for non-blocking: 50-200ms faster responses
|
|
664
|
+
|
|
640
665
|
## Resources
|
|
641
666
|
|
|
642
667
|
### Official Docs
|
|
@@ -644,6 +669,11 @@ if (process.env.NODE_ENV !== 'production') {
|
|
|
644
669
|
- App Router: https://nextjs.org/docs/app
|
|
645
670
|
- Learn: https://nextjs.org/learn
|
|
646
671
|
|
|
672
|
+
### Performance & Patterns
|
|
673
|
+
- Vercel Best Practices: https://github.com/vercel/react-best-practices
|
|
674
|
+
- See `references/rsc-patterns.md` - Detailed RSC & optimization guide
|
|
675
|
+
- See `references/performance.md` - Next.js performance optimizations
|
|
676
|
+
|
|
647
677
|
### Context7 Queries
|
|
648
678
|
|
|
649
679
|
```
|
|
@@ -651,4 +681,6 @@ Query: "Next.js 15 App Router data fetching"
|
|
|
651
681
|
Query: "Next.js Server Actions best practices"
|
|
652
682
|
Query: "Next.js middleware authentication"
|
|
653
683
|
Query: "Next.js Server Components patterns"
|
|
684
|
+
Query: "Next.js RSC waterfall elimination"
|
|
685
|
+
Query: "Next.js bundle optimization barrel imports"
|
|
654
686
|
```
|
|
@@ -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)
|
|
@@ -571,9 +571,30 @@ When reviewing React code:
|
|
|
571
571
|
- useDeferredValue (defer updates for performance)
|
|
572
572
|
- useSyncExternalStore (external store integration)
|
|
573
573
|
|
|
574
|
+
## Advanced Performance Patterns
|
|
575
|
+
|
|
576
|
+
**See `references/performance.md` for comprehensive optimization guide covering**:
|
|
577
|
+
- Re-render optimization (functional setState, lazy initialization, transitions)
|
|
578
|
+
- Rendering performance (hoisting, content-visibility, hydration)
|
|
579
|
+
- Client-side optimization (passive listeners, storage caching)
|
|
580
|
+
- JavaScript performance (index maps, Set/Map, toSorted)
|
|
581
|
+
- Advanced patterns (useLatest, event handler refs)
|
|
582
|
+
|
|
583
|
+
**Key takeaways**:
|
|
584
|
+
- Use functional setState to prevent stale closures
|
|
585
|
+
- Lazy initialize expensive state with function
|
|
586
|
+
- Mark non-urgent updates with startTransition
|
|
587
|
+
- Hoist static JSX outside render
|
|
588
|
+
- Use content-visibility for long lists
|
|
589
|
+
- Cache storage API calls in memory
|
|
590
|
+
- Build index Maps for repeated lookups
|
|
591
|
+
- Use toSorted() instead of sort() for immutability
|
|
592
|
+
|
|
574
593
|
## Resources
|
|
575
594
|
|
|
576
595
|
- [React Docs](https://react.dev) - Official documentation
|
|
577
596
|
- [React DevTools](https://react.dev/learn/react-developer-tools) - Browser extension
|
|
578
597
|
- [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks) - Hook constraints
|
|
579
598
|
- [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app) - TS patterns
|
|
599
|
+
- [Vercel React Best Practices](https://github.com/vercel/react-best-practices) - Performance patterns
|
|
600
|
+
- `references/performance.md` - Detailed optimization guide
|
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
# React Performance Optimization
|
|
2
|
+
|
|
3
|
+
> Critical performance patterns from Vercel Engineering
|
|
4
|
+
|
|
5
|
+
## Re-render Optimization
|
|
6
|
+
|
|
7
|
+
### Functional setState Updates
|
|
8
|
+
|
|
9
|
+
**Impact**: Prevents stale closures, enables stable callbacks
|
|
10
|
+
|
|
11
|
+
Always use functional updates when state depends on current state:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// ❌ Incorrect: stale closure risk
|
|
15
|
+
const removeItem = useCallback((id: string) => {
|
|
16
|
+
setItems(items.filter(item => item.id !== id))
|
|
17
|
+
}, []) // Missing items dependency - STALE
|
|
18
|
+
|
|
19
|
+
// ✅ Correct: always uses latest state
|
|
20
|
+
const removeItem = useCallback((id: string) => {
|
|
21
|
+
setItems(curr => curr.filter(item => item.id !== id))
|
|
22
|
+
}, []) // Safe and stable
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Lazy State Initialization
|
|
26
|
+
|
|
27
|
+
**Impact**: Expensive initial computation runs only once
|
|
28
|
+
|
|
29
|
+
Pass function to `useState` for expensive initial values:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// ❌ Incorrect: runs on every render
|
|
33
|
+
const [settings, setSettings] = useState(
|
|
34
|
+
JSON.parse(localStorage.getItem('settings') || '{}')
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
// ✅ Correct: runs only once
|
|
38
|
+
const [settings, setSettings] = useState(() => {
|
|
39
|
+
const stored = localStorage.getItem('settings')
|
|
40
|
+
return stored ? JSON.parse(stored) : {}
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Use Transitions for Non-Urgent Updates
|
|
45
|
+
|
|
46
|
+
**Impact**: Maintains UI responsiveness
|
|
47
|
+
|
|
48
|
+
Mark frequent, non-urgent updates as transitions:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { useTransition } from 'react'
|
|
52
|
+
|
|
53
|
+
function SearchResults() {
|
|
54
|
+
const [isPending, startTransition] = useTransition()
|
|
55
|
+
const [results, setResults] = useState([])
|
|
56
|
+
|
|
57
|
+
function handleSearch(query: string) {
|
|
58
|
+
startTransition(() => {
|
|
59
|
+
// Non-urgent: heavy filtering/search
|
|
60
|
+
setResults(expensiveSearch(query))
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<>
|
|
66
|
+
<input onChange={e => handleSearch(e.target.value)} />
|
|
67
|
+
{isPending && <Spinner />}
|
|
68
|
+
<Results data={results} />
|
|
69
|
+
</>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Defer State Reads to Usage Point
|
|
75
|
+
|
|
76
|
+
**Impact**: Avoids unnecessary re-renders
|
|
77
|
+
|
|
78
|
+
Don't subscribe to state if only read in callbacks:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// ❌ Incorrect: component re-renders on every URL change
|
|
82
|
+
function Component() {
|
|
83
|
+
const searchParams = useSearchParams() // Re-renders on change
|
|
84
|
+
|
|
85
|
+
const handleSubmit = () => {
|
|
86
|
+
const query = searchParams.get('q') // Used only here
|
|
87
|
+
submitForm(query)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return <button onClick={handleSubmit}>Submit</button>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ✅ Correct: no re-renders, read directly in callback
|
|
94
|
+
function Component() {
|
|
95
|
+
const handleSubmit = () => {
|
|
96
|
+
const query = new URLSearchParams(window.location.search).get('q')
|
|
97
|
+
submitForm(query)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return <button onClick={handleSubmit}>Submit</button>
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Extract to Memoized Components
|
|
105
|
+
|
|
106
|
+
**Impact**: Prevents unnecessary re-renders of expensive subtrees
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// ❌ Incorrect: entire list re-renders on count change
|
|
110
|
+
function Page() {
|
|
111
|
+
const [count, setCount] = useState(0)
|
|
112
|
+
return (
|
|
113
|
+
<div>
|
|
114
|
+
<button onClick={() => setCount(c => c + 1)}>{count}</button>
|
|
115
|
+
<ExpensiveList items={items} /> {/* Re-renders unnecessarily */}
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ✅ Correct: list doesn't re-render
|
|
121
|
+
const MemoizedList = memo(ExpensiveList)
|
|
122
|
+
|
|
123
|
+
function Page() {
|
|
124
|
+
const [count, setCount] = useState(0)
|
|
125
|
+
return (
|
|
126
|
+
<div>
|
|
127
|
+
<button onClick={() => setCount(c => c + 1)}>{count}</button>
|
|
128
|
+
<MemoizedList items={items} />
|
|
129
|
+
</div>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Narrow Effect Dependencies
|
|
135
|
+
|
|
136
|
+
**Impact**: Reduces effect re-runs
|
|
137
|
+
|
|
138
|
+
Extract only needed properties instead of whole objects:
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// ❌ Incorrect: effect runs on any user change
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
logUserActivity(user.id)
|
|
144
|
+
}, [user]) // user object changes often
|
|
145
|
+
|
|
146
|
+
// ✅ Correct: effect only runs when ID changes
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
logUserActivity(user.id)
|
|
149
|
+
}, [user.id]) // Stable value
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Subscribe to Derived State
|
|
153
|
+
|
|
154
|
+
**Impact**: Avoids storing and synchronizing computed values
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// ❌ Incorrect: derived state in useState (sync issue)
|
|
158
|
+
const [items, setItems] = useState([])
|
|
159
|
+
const [count, setCount] = useState(0)
|
|
160
|
+
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
setCount(items.length) // Sync issue!
|
|
163
|
+
}, [items])
|
|
164
|
+
|
|
165
|
+
// ✅ Correct: compute during render
|
|
166
|
+
const [items, setItems] = useState([])
|
|
167
|
+
const count = items.length // Always in sync
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Rendering Performance
|
|
171
|
+
|
|
172
|
+
### Hoist Static JSX Elements
|
|
173
|
+
|
|
174
|
+
**Impact**: Prevents recreation on every render
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// ❌ Incorrect: creates new object on every render
|
|
178
|
+
function Component({ title }) {
|
|
179
|
+
return (
|
|
180
|
+
<div>
|
|
181
|
+
<Icon name="check" color="green" /> {/* New object every render */}
|
|
182
|
+
{title}
|
|
183
|
+
</div>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ✅ Correct: created once
|
|
188
|
+
const CheckIcon = <Icon name="check" color="green" />
|
|
189
|
+
|
|
190
|
+
function Component({ title }) {
|
|
191
|
+
return (
|
|
192
|
+
<div>
|
|
193
|
+
{CheckIcon}
|
|
194
|
+
{title}
|
|
195
|
+
</div>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### CSS content-visibility for Long Lists
|
|
201
|
+
|
|
202
|
+
**Impact**: 10× faster initial render
|
|
203
|
+
|
|
204
|
+
Browser skips layout/paint for off-screen items:
|
|
205
|
+
|
|
206
|
+
```css
|
|
207
|
+
.message-item {
|
|
208
|
+
content-visibility: auto;
|
|
209
|
+
contain-intrinsic-size: 0 80px; /* Estimated height */
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Use Explicit Conditional Rendering
|
|
214
|
+
|
|
215
|
+
**Impact**: Prevents rendering unwanted values
|
|
216
|
+
|
|
217
|
+
Use ternary when condition can be `0` or `NaN`:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// ❌ Incorrect: renders "0" when count is 0
|
|
221
|
+
{count && <div>{count} items</div>}
|
|
222
|
+
|
|
223
|
+
// ✅ Correct: renders nothing when 0
|
|
224
|
+
{count > 0 && <div>{count} items</div>}
|
|
225
|
+
// or
|
|
226
|
+
{count ? <div>{count} items</div> : null}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Prevent Hydration Mismatch Without Flickering
|
|
230
|
+
|
|
231
|
+
**Impact**: No FOUC (Flash of Unstyled Content)
|
|
232
|
+
|
|
233
|
+
Use synchronous inline script:
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
function ThemeWrapper({ children }) {
|
|
237
|
+
return (
|
|
238
|
+
<>
|
|
239
|
+
<div id="theme-wrapper">{children}</div>
|
|
240
|
+
<script dangerouslySetInnerHTML={{__html: `
|
|
241
|
+
(function() {
|
|
242
|
+
try {
|
|
243
|
+
var theme = localStorage.getItem('theme') || 'light';
|
|
244
|
+
var el = document.getElementById('theme-wrapper');
|
|
245
|
+
if (el) el.className = theme;
|
|
246
|
+
} catch (e) {}
|
|
247
|
+
})();
|
|
248
|
+
`}} />
|
|
249
|
+
</>
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Animate SVG Wrapper Instead of Element
|
|
255
|
+
|
|
256
|
+
**Impact**: Reduces paint complexity
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
// ❌ Incorrect: animates SVG element directly
|
|
260
|
+
<svg style={{ transform: `translateX(${x}px)` }}>
|
|
261
|
+
<path d="..." />
|
|
262
|
+
</svg>
|
|
263
|
+
|
|
264
|
+
// ✅ Correct: animate wrapper div
|
|
265
|
+
<div style={{ transform: `translateX(${x}px)` }}>
|
|
266
|
+
<svg><path d="..." /></svg>
|
|
267
|
+
</div>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Activity Component for Show/Hide
|
|
271
|
+
|
|
272
|
+
**Impact**: Prevents layout thrashing
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
// ❌ Incorrect: removes from DOM (layout thrash)
|
|
276
|
+
{isVisible && <ExpensiveComponent />}
|
|
277
|
+
|
|
278
|
+
// ✅ Correct: uses visibility (keeps layout)
|
|
279
|
+
<div style={{ visibility: isVisible ? 'visible' : 'hidden' }}>
|
|
280
|
+
<ExpensiveComponent />
|
|
281
|
+
</div>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Client-Side Optimization
|
|
285
|
+
|
|
286
|
+
### Use Passive Event Listeners
|
|
287
|
+
|
|
288
|
+
**Impact**: Eliminates scroll delay
|
|
289
|
+
|
|
290
|
+
Add `{ passive: true }` to touch/wheel events:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
const handleScroll = (e: Event) => {
|
|
295
|
+
// No preventDefault() call
|
|
296
|
+
updateScrollPosition()
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ✅ Passive listener - doesn't block scrolling
|
|
300
|
+
window.addEventListener('scroll', handleScroll, { passive: true })
|
|
301
|
+
|
|
302
|
+
return () => {
|
|
303
|
+
window.removeEventListener('scroll', handleScroll, { passive: true } as any)
|
|
304
|
+
}
|
|
305
|
+
}, [])
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Deduplicate Global Event Listeners
|
|
309
|
+
|
|
310
|
+
**Impact**: N instances = 1 listener instead of N
|
|
311
|
+
|
|
312
|
+
Use shared hook for global events:
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
// ❌ Incorrect: N listeners for N components
|
|
316
|
+
function Component() {
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
const handler = () => console.log('resize')
|
|
319
|
+
window.addEventListener('resize', handler)
|
|
320
|
+
return () => window.removeEventListener('resize', handler)
|
|
321
|
+
}, [])
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ✅ Correct: 1 listener shared across all instances
|
|
325
|
+
import { useSWRSubscription } from 'swr/subscription'
|
|
326
|
+
|
|
327
|
+
function useWindowSize() {
|
|
328
|
+
return useSWRSubscription('window-size', (key, { next }) => {
|
|
329
|
+
const handler = () => next(null, {
|
|
330
|
+
width: window.innerWidth,
|
|
331
|
+
height: window.innerHeight
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
window.addEventListener('resize', handler)
|
|
335
|
+
handler() // Initial value
|
|
336
|
+
|
|
337
|
+
return () => window.removeEventListener('resize', handler)
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Cache Storage API Calls
|
|
343
|
+
|
|
344
|
+
**Impact**: Avoids synchronous I/O on every access
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
// ❌ Incorrect: reads from localStorage on every access
|
|
348
|
+
function useSettings() {
|
|
349
|
+
const [settings, setSettings] = useState(() => {
|
|
350
|
+
return JSON.parse(localStorage.getItem('settings') || '{}')
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
// Every time this runs, reads from localStorage
|
|
354
|
+
const value = localStorage.getItem('key')
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ✅ Correct: cache in memory
|
|
358
|
+
const settingsCache = new Map<string, any>()
|
|
359
|
+
|
|
360
|
+
function getCachedSetting(key: string) {
|
|
361
|
+
if (settingsCache.has(key)) {
|
|
362
|
+
return settingsCache.get(key)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const value = localStorage.getItem(key)
|
|
366
|
+
const parsed = value ? JSON.parse(value) : null
|
|
367
|
+
settingsCache.set(key, parsed)
|
|
368
|
+
return parsed
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Invalidate on storage events
|
|
372
|
+
window.addEventListener('storage', (e) => {
|
|
373
|
+
if (e.key) settingsCache.delete(e.key)
|
|
374
|
+
})
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## JavaScript Performance
|
|
378
|
+
|
|
379
|
+
### Build Index Maps for Repeated Lookups
|
|
380
|
+
|
|
381
|
+
**Impact**: O(n×m) → O(n+m), 1M ops → 2K ops
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// ❌ Incorrect: O(n) per lookup
|
|
385
|
+
return orders.map(order => ({
|
|
386
|
+
...order,
|
|
387
|
+
user: users.find(u => u.id === order.userId) // O(n) each time
|
|
388
|
+
}))
|
|
389
|
+
|
|
390
|
+
// ✅ Correct: O(1) per lookup
|
|
391
|
+
const userById = new Map(users.map(u => [u.id, u]))
|
|
392
|
+
return orders.map(order => ({
|
|
393
|
+
...order,
|
|
394
|
+
user: userById.get(order.userId) // O(1)
|
|
395
|
+
}))
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Cache Property Access in Loops
|
|
399
|
+
|
|
400
|
+
**Impact**: Avoids repeated property lookups
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
// ❌ Incorrect: repeated access
|
|
404
|
+
for (let i = 0; i < items.length; i++) {
|
|
405
|
+
// items.length evaluated each iteration
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ✅ Correct: cached
|
|
409
|
+
const len = items.length
|
|
410
|
+
for (let i = 0; i < len; i++) {
|
|
411
|
+
// Faster
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Hoist RegExp Creation
|
|
416
|
+
|
|
417
|
+
**Impact**: Avoids creating regex on every call
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
// ❌ Incorrect: creates new RegExp every call
|
|
421
|
+
function validate(email: string) {
|
|
422
|
+
return /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(email)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ✅ Correct: created once
|
|
426
|
+
const EMAIL_REGEX = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/
|
|
427
|
+
|
|
428
|
+
function validate(email: string) {
|
|
429
|
+
return EMAIL_REGEX.test(email)
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Use toSorted() Instead of sort()
|
|
434
|
+
|
|
435
|
+
**Impact**: Prevents mutation bugs in React state
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
// ❌ Incorrect: mutates array
|
|
439
|
+
setItems(items.sort((a, b) => a - b)) // MUTATION BUG
|
|
440
|
+
|
|
441
|
+
// ✅ Correct: creates new array (Chrome 110+)
|
|
442
|
+
setItems(items.toSorted((a, b) => a - b))
|
|
443
|
+
|
|
444
|
+
// ✅ Alternative: spread + sort
|
|
445
|
+
setItems([...items].sort((a, b) => a - b))
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Combine Multiple Array Iterations
|
|
449
|
+
|
|
450
|
+
**Impact**: O(3n) → O(n)
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
// ❌ Incorrect: 3 passes
|
|
454
|
+
const filtered = items.filter(x => x.active)
|
|
455
|
+
const mapped = filtered.map(x => x.value)
|
|
456
|
+
const result = mapped.slice(0, 10)
|
|
457
|
+
|
|
458
|
+
// ✅ Correct: 1 pass
|
|
459
|
+
const result = items
|
|
460
|
+
.filter(x => x.active)
|
|
461
|
+
.map(x => x.value)
|
|
462
|
+
.slice(0, 10)
|
|
463
|
+
|
|
464
|
+
// ✅ Even better: reduce for complex transforms
|
|
465
|
+
const result = items.reduce((acc, item) => {
|
|
466
|
+
if (item.active && acc.length < 10) {
|
|
467
|
+
acc.push(item.value)
|
|
468
|
+
}
|
|
469
|
+
return acc
|
|
470
|
+
}, [] as number[])
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Use Set/Map for O(1) Lookups
|
|
474
|
+
|
|
475
|
+
**Impact**: O(n) → O(1)
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
// ❌ Incorrect: O(n) lookup
|
|
479
|
+
const isAllowed = (id: string) => allowedIds.includes(id)
|
|
480
|
+
|
|
481
|
+
// ✅ Correct: O(1) lookup
|
|
482
|
+
const allowedSet = new Set(allowedIds)
|
|
483
|
+
const isAllowed = (id: string) => allowedSet.has(id)
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Early Length Check for Array Comparisons
|
|
487
|
+
|
|
488
|
+
**Impact**: Avoids unnecessary iteration
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
// ❌ Incorrect: iterates even when lengths differ
|
|
492
|
+
function arraysEqual(a: any[], b: any[]) {
|
|
493
|
+
return a.every((item, i) => item === b[i])
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ✅ Correct: check length first
|
|
497
|
+
function arraysEqual(a: any[], b: any[]) {
|
|
498
|
+
if (a.length !== b.length) return false
|
|
499
|
+
return a.every((item, i) => item === b[i])
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
## Advanced Patterns
|
|
504
|
+
|
|
505
|
+
### Store Event Handlers in Refs
|
|
506
|
+
|
|
507
|
+
**Impact**: Stable references without useCallback
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
// ❌ Incorrect: needs dependencies
|
|
511
|
+
const handleClick = useCallback(() => {
|
|
512
|
+
doSomething(prop1, prop2)
|
|
513
|
+
}, [prop1, prop2]) // Changes often
|
|
514
|
+
|
|
515
|
+
// ✅ Correct: always stable
|
|
516
|
+
const handleClickRef = useRef<() => void>()
|
|
517
|
+
handleClickRef.current = () => {
|
|
518
|
+
doSomething(prop1, prop2)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const handleClick = useCallback(() => {
|
|
522
|
+
handleClickRef.current?.()
|
|
523
|
+
}, []) // Never changes
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### useLatest for Stable Callback Refs
|
|
527
|
+
|
|
528
|
+
**Impact**: Avoids stale closures with stable identity
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
function useLatest<T>(value: T) {
|
|
532
|
+
const ref = useRef(value)
|
|
533
|
+
useEffect(() => {
|
|
534
|
+
ref.current = value
|
|
535
|
+
})
|
|
536
|
+
return ref
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Usage
|
|
540
|
+
function Component({ onEvent }) {
|
|
541
|
+
const onEventRef = useLatest(onEvent)
|
|
542
|
+
|
|
543
|
+
useEffect(() => {
|
|
544
|
+
const handler = () => onEventRef.current()
|
|
545
|
+
window.addEventListener('resize', handler)
|
|
546
|
+
return () => window.removeEventListener('resize', handler)
|
|
547
|
+
}, []) // Empty deps, always uses latest
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
## Anti-Patterns to Avoid
|
|
552
|
+
|
|
553
|
+
1. ❌ Non-functional setState (stale closures)
|
|
554
|
+
2. ❌ Missing lazy initialization for expensive initial state
|
|
555
|
+
3. ❌ State reads in render when only used in callbacks
|
|
556
|
+
4. ❌ Creating RegExp/objects in render
|
|
557
|
+
5. ❌ Non-passive scroll/touch event listeners
|
|
558
|
+
6. ❌ Multiple array iterations when one would suffice
|
|
559
|
+
7. ❌ Using `&&` for conditional rendering with numbers
|
|
560
|
+
8. ❌ Direct `.sort()` mutation in React state
|
|
561
|
+
9. ❌ Storing derived values in state
|
|
562
|
+
10. ❌ Not extracting expensive components to memo
|
|
563
|
+
|
|
564
|
+
## React Compiler Note
|
|
565
|
+
|
|
566
|
+
Many of these optimizations become automatic with React Compiler:
|
|
567
|
+
- Auto-memoization of expensive computations
|
|
568
|
+
- Automatic extraction of stable dependencies
|
|
569
|
+
- Smart re-render optimization
|
|
570
|
+
|
|
571
|
+
However, architectural patterns (lazy state init, transitions, content-visibility) still require manual implementation.
|
|
572
|
+
|
|
573
|
+
**Reference**: [Vercel React Best Practices](https://github.com/vercel/react-best-practices)
|