dev3000 0.0.147 → 0.0.148
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/dist/cli.js +10 -7
- package/dist/cli.js.map +1 -1
- package/dist/components/SkillSelector.d.ts +2 -2
- package/dist/components/SkillSelector.d.ts.map +1 -1
- package/dist/components/SkillSelector.js +10 -4
- package/dist/components/SkillSelector.js.map +1 -1
- package/dist/dev-environment.d.ts +38 -0
- package/dist/dev-environment.d.ts.map +1 -1
- package/dist/dev-environment.js +60 -10
- package/dist/dev-environment.js.map +1 -1
- package/dist/skills/index.d.ts +3 -2
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +8 -4
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/index.ts +9 -4
- package/dist/utils/skill-installer.d.ts +4 -2
- package/dist/utils/skill-installer.d.ts.map +1 -1
- package/dist/utils/skill-installer.js +78 -10
- package/dist/utils/skill-installer.js.map +1 -1
- package/mcp-server/.next/BUILD_ID +1 -1
- package/mcp-server/.next/build-manifest.json +2 -2
- package/mcp-server/.next/fallback-build-manifest.json +2 -2
- package/mcp-server/.next/prerender-manifest.json +3 -3
- package/mcp-server/.next/server/app/_global-error.html +2 -2
- package/mcp-server/.next/server/app/_global-error.rsc +1 -1
- package/mcp-server/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found.html +1 -1
- package/mcp-server/.next/server/app/_not-found.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/mcp-server/.next/server/app/index.html +1 -1
- package/mcp-server/.next/server/app/index.rsc +1 -1
- package/mcp-server/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/mcp-server/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/mcp-server/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/mcp-server/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/mcp-server/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/mcp-server/.next/server/pages/404.html +1 -1
- package/mcp-server/.next/server/pages/500.html +2 -2
- package/mcp-server/.next/server/server-reference-manifest.js +1 -1
- package/mcp-server/.next/server/server-reference-manifest.json +1 -1
- package/package.json +3 -3
- package/dist/skills/react-performance/SKILL.md +0 -1034
- /package/mcp-server/.next/static/{2wDj8SEBvulPHjApt1crR → 5msMCZw04bKnsG5w2WyDQ}/_buildManifest.js +0 -0
- /package/mcp-server/.next/static/{2wDj8SEBvulPHjApt1crR → 5msMCZw04bKnsG5w2WyDQ}/_clientMiddlewareManifest.json +0 -0
- /package/mcp-server/.next/static/{2wDj8SEBvulPHjApt1crR → 5msMCZw04bKnsG5w2WyDQ}/_ssgManifest.js +0 -0
|
@@ -1,1034 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: React performance optimization guidelines. Use when optimizing React/Next.js apps, fixing performance issues, reducing bundle size, eliminating waterfalls, or improving rendering speed.
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# React Performance Guidelines
|
|
6
|
-
|
|
7
|
-
**Version 1.0**
|
|
8
|
-
Vercel Engineering
|
|
9
|
-
January 2026
|
|
10
|
-
|
|
11
|
-
> **Note:**
|
|
12
|
-
> This document is mainly for agents and LLMs to follow when maintaining,
|
|
13
|
-
> generating, or refactoring React and Next.js codebases at Vercel. Humans
|
|
14
|
-
> may also find it useful, but guidance here is optimized for automation
|
|
15
|
-
> and consistency by AI-assisted workflows.
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## Abstract
|
|
20
|
-
|
|
21
|
-
Performance optimization guide for React and Next.js applications,
|
|
22
|
-
ordered by impact. Sections 1-2 yield the highest gains (2-10x
|
|
23
|
-
improvements), sections 3-5 provide medium gains (20-50%), and
|
|
24
|
-
sections 6-8 offer incremental improvements (5-20%) in hot paths.
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Table of Contents
|
|
29
|
-
|
|
30
|
-
1. [Eliminating Waterfalls](#1-eliminating-waterfalls) - **CRITICAL**
|
|
31
|
-
2. [Bundle Size Optimization](#2-bundle-size-optimization) - **CRITICAL**
|
|
32
|
-
3. [Server-Side Performance](#3-server-side-performance) - **HIGH**
|
|
33
|
-
4. [Client-Side Data Fetching](#4-client-side-data-fetching) - **MEDIUM-HIGH**
|
|
34
|
-
5. [Re-render Optimization](#5-re-render-optimization) - **MEDIUM**
|
|
35
|
-
6. [Rendering Performance](#6-rendering-performance) - **MEDIUM**
|
|
36
|
-
7. [JavaScript Performance](#7-javascript-performance) - **LOW-MEDIUM**
|
|
37
|
-
8. [Advanced Patterns](#8-advanced-patterns) - **LOW**
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## 1. Eliminating Waterfalls
|
|
42
|
-
|
|
43
|
-
**Impact: CRITICAL (2-10x improvement)**
|
|
44
|
-
|
|
45
|
-
Waterfalls are the #1 performance killer. Each sequential await
|
|
46
|
-
adds full network latency. Eliminating them yields the largest
|
|
47
|
-
gains.
|
|
48
|
-
|
|
49
|
-
### 1.1 Promise.all() for Independent Operations
|
|
50
|
-
|
|
51
|
-
When async operations have no interdependencies, execute them
|
|
52
|
-
concurrently using `Promise.all()`.
|
|
53
|
-
|
|
54
|
-
**Incorrect (sequential execution, 3 round trips):**
|
|
55
|
-
|
|
56
|
-
```typescript
|
|
57
|
-
const user = await fetchUser()
|
|
58
|
-
const posts = await fetchPosts()
|
|
59
|
-
const comments = await fetchComments()
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
**Correct (parallel execution, 1 round trip):**
|
|
63
|
-
|
|
64
|
-
```typescript
|
|
65
|
-
const [user, posts, comments] = await Promise.all([
|
|
66
|
-
fetchUser(),
|
|
67
|
-
fetchPosts(),
|
|
68
|
-
fetchComments()
|
|
69
|
-
])
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### 1.2 Dependency-Based Parallelization
|
|
73
|
-
|
|
74
|
-
For operations with partial dependencies, use `better-all` to
|
|
75
|
-
maximize parallelism. It automatically starts each task at the
|
|
76
|
-
earliest possible moment.
|
|
77
|
-
|
|
78
|
-
**Incorrect (profile waits for config unnecessarily):**
|
|
79
|
-
|
|
80
|
-
```typescript
|
|
81
|
-
const [user, config] = await Promise.all([
|
|
82
|
-
fetchUser(),
|
|
83
|
-
fetchConfig()
|
|
84
|
-
])
|
|
85
|
-
const profile = await fetchProfile(user.id)
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
**Correct (config and profile run in parallel):**
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
import { all } from 'better-all'
|
|
92
|
-
|
|
93
|
-
const { user, config, profile } = await all({
|
|
94
|
-
async user() { return fetchUser() },
|
|
95
|
-
async config() { return fetchConfig() },
|
|
96
|
-
async profile() {
|
|
97
|
-
return fetchProfile((await this.$.user).id)
|
|
98
|
-
}
|
|
99
|
-
})
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
Reference: [better-all](https://github.com/shuding/better-all)
|
|
103
|
-
|
|
104
|
-
### 1.3 Prevent Waterfall Chains in API Routes
|
|
105
|
-
|
|
106
|
-
In API routes and Server Actions, start independent operations
|
|
107
|
-
immediately, even if you don't await them yet.
|
|
108
|
-
|
|
109
|
-
**Incorrect (config waits for auth, data waits for both):**
|
|
110
|
-
|
|
111
|
-
```typescript
|
|
112
|
-
export async function GET(request: Request) {
|
|
113
|
-
const session = await auth()
|
|
114
|
-
const config = await fetchConfig()
|
|
115
|
-
const data = await fetchData(session.user.id)
|
|
116
|
-
return Response.json({ data, config })
|
|
117
|
-
}
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
**Correct (auth and config start immediately):**
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
export async function GET(request: Request) {
|
|
124
|
-
const sessionPromise = auth()
|
|
125
|
-
const configPromise = fetchConfig()
|
|
126
|
-
const session = await sessionPromise
|
|
127
|
-
const [config, data] = await Promise.all([
|
|
128
|
-
configPromise,
|
|
129
|
-
fetchData(session.user.id)
|
|
130
|
-
])
|
|
131
|
-
return Response.json({ data, config })
|
|
132
|
-
}
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
---
|
|
136
|
-
|
|
137
|
-
## 2. Bundle Size Optimization
|
|
138
|
-
|
|
139
|
-
**Impact: CRITICAL (directly affects TTI and LCP)**
|
|
140
|
-
|
|
141
|
-
Reducing initial bundle size improves Time to Interactive and
|
|
142
|
-
Largest Contentful Paint.
|
|
143
|
-
|
|
144
|
-
### 2.1 Dynamic Imports for Heavy Components
|
|
145
|
-
|
|
146
|
-
Use `next/dynamic` to lazy-load large components not needed on
|
|
147
|
-
initial render.
|
|
148
|
-
|
|
149
|
-
**Incorrect (Monaco bundles with main chunk ~300KB):**
|
|
150
|
-
|
|
151
|
-
```tsx
|
|
152
|
-
import { MonacoEditor } from './monaco-editor'
|
|
153
|
-
|
|
154
|
-
function CodePanel({ code }: { code: string }) {
|
|
155
|
-
return <MonacoEditor value={code} />
|
|
156
|
-
}
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
**Correct (Monaco loads on demand):**
|
|
160
|
-
|
|
161
|
-
```tsx
|
|
162
|
-
import dynamic from 'next/dynamic'
|
|
163
|
-
|
|
164
|
-
const MonacoEditor = dynamic(
|
|
165
|
-
() => import('./monaco-editor').then(m => m.MonacoEditor),
|
|
166
|
-
{ ssr: false }
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
function CodePanel({ code }: { code: string }) {
|
|
170
|
-
return <MonacoEditor value={code} />
|
|
171
|
-
}
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
### 2.2 Preload Based on User Intent
|
|
175
|
-
|
|
176
|
-
Preload heavy bundles before they're needed to reduce perceived
|
|
177
|
-
latency.
|
|
178
|
-
|
|
179
|
-
**Example (preload on hover/focus):**
|
|
180
|
-
|
|
181
|
-
```tsx
|
|
182
|
-
function EditorButton({ onClick }: { onClick: () => void }) {
|
|
183
|
-
const preload = () => {
|
|
184
|
-
void import('./monaco-editor')
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return (
|
|
188
|
-
<button
|
|
189
|
-
onMouseEnter={preload}
|
|
190
|
-
onFocus={preload}
|
|
191
|
-
onClick={onClick}
|
|
192
|
-
>
|
|
193
|
-
Open Editor
|
|
194
|
-
</button>
|
|
195
|
-
)
|
|
196
|
-
}
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
### 2.3 Conditional Module Loading
|
|
200
|
-
|
|
201
|
-
Load large data or modules only when a feature is activated.
|
|
202
|
-
|
|
203
|
-
**Example (lazy-load animation frames):**
|
|
204
|
-
|
|
205
|
-
```tsx
|
|
206
|
-
function AnimationPlayer({ enabled }: { enabled: boolean }) {
|
|
207
|
-
const [frames, setFrames] = useState<Frame[] | null>(null)
|
|
208
|
-
|
|
209
|
-
useEffect(() => {
|
|
210
|
-
if (enabled && !frames) {
|
|
211
|
-
import('./animation-frames.js')
|
|
212
|
-
.then(mod => setFrames(mod.frames))
|
|
213
|
-
.catch(() => setEnabled(false))
|
|
214
|
-
}
|
|
215
|
-
}, [enabled, frames])
|
|
216
|
-
|
|
217
|
-
if (!frames) return <Skeleton />
|
|
218
|
-
return <Canvas frames={frames} />
|
|
219
|
-
}
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
### 2.4 Defer Non-Critical Third-Party Libraries
|
|
223
|
-
|
|
224
|
-
Analytics, logging, and error tracking don't block user
|
|
225
|
-
interaction. Load them after hydration.
|
|
226
|
-
|
|
227
|
-
**Incorrect (blocks initial bundle):**
|
|
228
|
-
|
|
229
|
-
```tsx
|
|
230
|
-
import { Analytics } from '@vercel/analytics/react'
|
|
231
|
-
|
|
232
|
-
export default function RootLayout({ children }) {
|
|
233
|
-
return (
|
|
234
|
-
<html>
|
|
235
|
-
<body>
|
|
236
|
-
{children}
|
|
237
|
-
<Analytics />
|
|
238
|
-
</body>
|
|
239
|
-
</html>
|
|
240
|
-
)
|
|
241
|
-
}
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
**Correct (loads after hydration):**
|
|
245
|
-
|
|
246
|
-
```tsx
|
|
247
|
-
import dynamic from 'next/dynamic'
|
|
248
|
-
|
|
249
|
-
const Analytics = dynamic(
|
|
250
|
-
() => import('@vercel/analytics/react').then(m => m.Analytics),
|
|
251
|
-
{ ssr: false }
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
export default function RootLayout({ children }) {
|
|
255
|
-
return (
|
|
256
|
-
<html>
|
|
257
|
-
<body>
|
|
258
|
-
{children}
|
|
259
|
-
<Analytics />
|
|
260
|
-
</body>
|
|
261
|
-
</html>
|
|
262
|
-
)
|
|
263
|
-
}
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
---
|
|
267
|
-
|
|
268
|
-
## 3. Server-Side Performance
|
|
269
|
-
|
|
270
|
-
**Impact: HIGH (eliminates server-side waterfalls)**
|
|
271
|
-
|
|
272
|
-
### 3.1 Parallel Data Fetching with Component Composition
|
|
273
|
-
|
|
274
|
-
React Server Components execute sequentially within a tree.
|
|
275
|
-
Restructure with composition to parallelize data fetching.
|
|
276
|
-
|
|
277
|
-
**Incorrect (Sidebar waits for Page's fetch to complete):**
|
|
278
|
-
|
|
279
|
-
```tsx
|
|
280
|
-
export default async function Page() {
|
|
281
|
-
const header = await fetchHeader()
|
|
282
|
-
return (
|
|
283
|
-
<div>
|
|
284
|
-
<div>{header}</div>
|
|
285
|
-
<Sidebar />
|
|
286
|
-
</div>
|
|
287
|
-
)
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
async function Sidebar() {
|
|
291
|
-
const items = await fetchSidebarItems()
|
|
292
|
-
return <nav>{items.map(renderItem)}</nav>
|
|
293
|
-
}
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
**Correct (both fetch simultaneously):**
|
|
297
|
-
|
|
298
|
-
```tsx
|
|
299
|
-
async function Header() {
|
|
300
|
-
const data = await fetchHeader()
|
|
301
|
-
return <div>{data}</div>
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
async function Sidebar() {
|
|
305
|
-
const items = await fetchSidebarItems()
|
|
306
|
-
return <nav>{items.map(renderItem)}</nav>
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
export default function Page() {
|
|
310
|
-
return (
|
|
311
|
-
<div>
|
|
312
|
-
<Header />
|
|
313
|
-
<Sidebar />
|
|
314
|
-
</div>
|
|
315
|
-
)
|
|
316
|
-
}
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
### 3.2 Minimize Serialization at RSC Boundaries
|
|
320
|
-
|
|
321
|
-
The React Server/Client boundary serializes all object
|
|
322
|
-
properties. Only pass fields that the client actually uses.
|
|
323
|
-
|
|
324
|
-
**Incorrect (serializes all 50 fields):**
|
|
325
|
-
|
|
326
|
-
```tsx
|
|
327
|
-
async function Page() {
|
|
328
|
-
const user = await fetchUser() // 50 fields
|
|
329
|
-
return <Profile user={user} />
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
'use client'
|
|
333
|
-
function Profile({ user }: { user: User }) {
|
|
334
|
-
return <div>{user.name}</div> // uses 1 field
|
|
335
|
-
}
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
**Correct (serializes only 1 field):**
|
|
339
|
-
|
|
340
|
-
```tsx
|
|
341
|
-
async function Page() {
|
|
342
|
-
const user = await fetchUser()
|
|
343
|
-
return <Profile name={user.name} />
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
'use client'
|
|
347
|
-
function Profile({ name }: { name: string }) {
|
|
348
|
-
return <div>{name}</div>
|
|
349
|
-
}
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
### 3.3 Per-Request Deduplication with React.cache()
|
|
353
|
-
|
|
354
|
-
Use `React.cache()` for server-side request deduplication.
|
|
355
|
-
Authentication and database queries benefit most.
|
|
356
|
-
|
|
357
|
-
**Usage:**
|
|
358
|
-
|
|
359
|
-
```typescript
|
|
360
|
-
import { cache } from 'react'
|
|
361
|
-
|
|
362
|
-
export const getCurrentUser = cache(async () => {
|
|
363
|
-
const session = await auth()
|
|
364
|
-
if (!session?.user?.id) return null
|
|
365
|
-
return await db.user.findUnique({
|
|
366
|
-
where: { id: session.user.id }
|
|
367
|
-
})
|
|
368
|
-
})
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
Within a single request, multiple calls to `getCurrentUser()`
|
|
372
|
-
execute the query only once.
|
|
373
|
-
|
|
374
|
-
### 3.4 Cross-Request LRU Caching
|
|
375
|
-
|
|
376
|
-
`React.cache()` only works within one request. For data shared
|
|
377
|
-
across sequential requests (user clicks button A then button B),
|
|
378
|
-
use an LRU cache.
|
|
379
|
-
|
|
380
|
-
**Implementation:**
|
|
381
|
-
|
|
382
|
-
```typescript
|
|
383
|
-
import { LRUCache } from 'lru-cache'
|
|
384
|
-
|
|
385
|
-
const cache = new LRUCache<string, any>({
|
|
386
|
-
max: 1000,
|
|
387
|
-
ttl: 5 * 60 * 1000 // 5 minutes
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
export async function getUser(id: string) {
|
|
391
|
-
const cached = cache.get(id)
|
|
392
|
-
if (cached) return cached
|
|
393
|
-
|
|
394
|
-
const user = await db.user.findUnique({ where: { id } })
|
|
395
|
-
cache.set(id, user)
|
|
396
|
-
return user
|
|
397
|
-
}
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
Use when sequential user actions hit multiple endpoints needing
|
|
401
|
-
the same data within seconds. In serverless, consider Redis for
|
|
402
|
-
cross-process caching.
|
|
403
|
-
|
|
404
|
-
---
|
|
405
|
-
|
|
406
|
-
## 4. Client-Side Data Fetching
|
|
407
|
-
|
|
408
|
-
**Impact: MEDIUM-HIGH (automatic deduplication)**
|
|
409
|
-
|
|
410
|
-
### 4.1 Use SWR for Automatic Deduplication
|
|
411
|
-
|
|
412
|
-
SWR enables request deduplication, caching, and revalidation
|
|
413
|
-
across component instances.
|
|
414
|
-
|
|
415
|
-
**Incorrect (no deduplication, each instance fetches):**
|
|
416
|
-
|
|
417
|
-
```tsx
|
|
418
|
-
function UserList() {
|
|
419
|
-
const [users, setUsers] = useState([])
|
|
420
|
-
useEffect(() => {
|
|
421
|
-
fetch('/api/users')
|
|
422
|
-
.then(r => r.json())
|
|
423
|
-
.then(setUsers)
|
|
424
|
-
}, [])
|
|
425
|
-
}
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
**Correct (multiple instances share one request):**
|
|
429
|
-
|
|
430
|
-
```tsx
|
|
431
|
-
import useSWR from 'swr'
|
|
432
|
-
|
|
433
|
-
function UserList() {
|
|
434
|
-
const { data: users } = useSWR('/api/users', fetcher)
|
|
435
|
-
}
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
### 4.2 Deduplicate Global Event Listeners
|
|
439
|
-
|
|
440
|
-
Use `useSWRSubscription()` to share global event listeners
|
|
441
|
-
across component instances.
|
|
442
|
-
|
|
443
|
-
**Incorrect (N instances = N listeners):**
|
|
444
|
-
|
|
445
|
-
```tsx
|
|
446
|
-
function KeyboardShortcut({ onTrigger }: Props) {
|
|
447
|
-
useEffect(() => {
|
|
448
|
-
const handler = (e: KeyboardEvent) => {
|
|
449
|
-
if (e.metaKey && e.key === 'k') {
|
|
450
|
-
onTrigger()
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
window.addEventListener('keydown', handler)
|
|
454
|
-
return () => window.removeEventListener('keydown', handler)
|
|
455
|
-
}, [onTrigger])
|
|
456
|
-
}
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
**Correct (N instances = 1 listener):**
|
|
460
|
-
|
|
461
|
-
```tsx
|
|
462
|
-
import useSWRSubscription from 'swr/subscription'
|
|
463
|
-
|
|
464
|
-
function useKeyboardShortcut(key: string, callback: () => void) {
|
|
465
|
-
useSWRSubscription(['keydown', key], (_, { next }) => {
|
|
466
|
-
const handler = (e: KeyboardEvent) => {
|
|
467
|
-
if (e.metaKey && e.key === key) {
|
|
468
|
-
next(null, e)
|
|
469
|
-
callback()
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
window.addEventListener('keydown', handler)
|
|
473
|
-
return () => window.removeEventListener('keydown', handler)
|
|
474
|
-
})
|
|
475
|
-
}
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
---
|
|
479
|
-
|
|
480
|
-
## 5. Re-render Optimization
|
|
481
|
-
|
|
482
|
-
**Impact: MEDIUM (reduces unnecessary work)**
|
|
483
|
-
|
|
484
|
-
### 5.1 Subscribe to Derived State
|
|
485
|
-
|
|
486
|
-
Subscribe to derived boolean state instead of continuous values
|
|
487
|
-
to reduce re-render frequency.
|
|
488
|
-
|
|
489
|
-
**Incorrect (re-renders on every pixel change):**
|
|
490
|
-
|
|
491
|
-
```tsx
|
|
492
|
-
function Sidebar() {
|
|
493
|
-
const width = useWindowWidth() // updates continuously
|
|
494
|
-
const isMobile = width < 768
|
|
495
|
-
return <nav className={isMobile ? 'mobile' : 'desktop'}>
|
|
496
|
-
}
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
**Correct (re-renders only when boolean changes):**
|
|
500
|
-
|
|
501
|
-
```tsx
|
|
502
|
-
function Sidebar() {
|
|
503
|
-
const isMobile = useMediaQuery('(max-width: 767px)')
|
|
504
|
-
return <nav className={isMobile ? 'mobile' : 'desktop'}>
|
|
505
|
-
}
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
### 5.2 Narrow Effect Dependencies
|
|
509
|
-
|
|
510
|
-
Specify primitive dependencies instead of objects to minimize
|
|
511
|
-
effect re-runs.
|
|
512
|
-
|
|
513
|
-
**Incorrect (re-runs on any user field change):**
|
|
514
|
-
|
|
515
|
-
```tsx
|
|
516
|
-
useEffect(() => {
|
|
517
|
-
console.log(user.id)
|
|
518
|
-
}, [user])
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
**Correct (re-runs only when id changes):**
|
|
522
|
-
|
|
523
|
-
```tsx
|
|
524
|
-
useEffect(() => {
|
|
525
|
-
console.log(user.id)
|
|
526
|
-
}, [user.id])
|
|
527
|
-
```
|
|
528
|
-
|
|
529
|
-
### 5.3 Use Transitions for Non-Urgent Updates
|
|
530
|
-
|
|
531
|
-
Mark frequent, non-urgent state updates as transitions to
|
|
532
|
-
maintain UI responsiveness.
|
|
533
|
-
|
|
534
|
-
**Incorrect (blocks UI on every scroll):**
|
|
535
|
-
|
|
536
|
-
```tsx
|
|
537
|
-
function ScrollTracker() {
|
|
538
|
-
const [scrollY, setScrollY] = useState(0)
|
|
539
|
-
useEffect(() => {
|
|
540
|
-
const handler = () => setScrollY(window.scrollY)
|
|
541
|
-
window.addEventListener('scroll', handler, { passive: true })
|
|
542
|
-
return () => window.removeEventListener('scroll', handler)
|
|
543
|
-
}, [])
|
|
544
|
-
}
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
**Correct (non-blocking updates):**
|
|
548
|
-
|
|
549
|
-
```tsx
|
|
550
|
-
import { startTransition } from 'react'
|
|
551
|
-
|
|
552
|
-
function ScrollTracker() {
|
|
553
|
-
const [scrollY, setScrollY] = useState(0)
|
|
554
|
-
useEffect(() => {
|
|
555
|
-
const handler = () => {
|
|
556
|
-
startTransition(() => setScrollY(window.scrollY))
|
|
557
|
-
}
|
|
558
|
-
window.addEventListener('scroll', handler, { passive: true })
|
|
559
|
-
return () => window.removeEventListener('scroll', handler)
|
|
560
|
-
}, [])
|
|
561
|
-
}
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
### 5.4 Extract to Memoized Components
|
|
565
|
-
|
|
566
|
-
Extract expensive work into memoized components to enable early
|
|
567
|
-
returns before computation.
|
|
568
|
-
|
|
569
|
-
**Incorrect (computes avatar even when loading):**
|
|
570
|
-
|
|
571
|
-
```tsx
|
|
572
|
-
function Profile({ user, loading }: Props) {
|
|
573
|
-
const avatar = useMemo(() => {
|
|
574
|
-
const id = computeAvatarId(user)
|
|
575
|
-
return <Avatar id={id} />
|
|
576
|
-
}, [user])
|
|
577
|
-
|
|
578
|
-
if (loading) return <Skeleton />
|
|
579
|
-
return <div>{avatar}</div>
|
|
580
|
-
}
|
|
581
|
-
```
|
|
582
|
-
|
|
583
|
-
**Correct (skips computation when loading):**
|
|
584
|
-
|
|
585
|
-
```tsx
|
|
586
|
-
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
|
|
587
|
-
const id = useMemo(() => computeAvatarId(user), [user])
|
|
588
|
-
return <Avatar id={id} />
|
|
589
|
-
})
|
|
590
|
-
|
|
591
|
-
function Profile({ user, loading }: Props) {
|
|
592
|
-
if (loading) return <Skeleton />
|
|
593
|
-
return (
|
|
594
|
-
<div>
|
|
595
|
-
<UserAvatar user={user} />
|
|
596
|
-
</div>
|
|
597
|
-
)
|
|
598
|
-
}
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
### 5.5 Defer State Reads to Usage Point
|
|
602
|
-
|
|
603
|
-
Don't subscribe to dynamic state (searchParams, localStorage)
|
|
604
|
-
if you only read it inside callbacks.
|
|
605
|
-
|
|
606
|
-
**Incorrect (subscribes to all searchParams changes):**
|
|
607
|
-
|
|
608
|
-
```tsx
|
|
609
|
-
function ShareButton({ chatId }: { chatId: string }) {
|
|
610
|
-
const searchParams = useSearchParams()
|
|
611
|
-
|
|
612
|
-
const handleShare = () => {
|
|
613
|
-
const ref = searchParams.get('ref')
|
|
614
|
-
shareChat(chatId, { ref })
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
return <button onClick={handleShare}>Share</button>
|
|
618
|
-
}
|
|
619
|
-
```
|
|
620
|
-
|
|
621
|
-
**Correct (reads on demand, no subscription):**
|
|
622
|
-
|
|
623
|
-
```tsx
|
|
624
|
-
function ShareButton({ chatId }: { chatId: string }) {
|
|
625
|
-
const handleShare = () => {
|
|
626
|
-
const params = new URLSearchParams(window.location.search)
|
|
627
|
-
const ref = params.get('ref')
|
|
628
|
-
shareChat(chatId, { ref })
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
return <button onClick={handleShare}>Share</button>
|
|
632
|
-
}
|
|
633
|
-
```
|
|
634
|
-
|
|
635
|
-
---
|
|
636
|
-
|
|
637
|
-
## 6. Rendering Performance
|
|
638
|
-
|
|
639
|
-
**Impact: MEDIUM (reduces rendering work)**
|
|
640
|
-
|
|
641
|
-
### 6.1 Hoist Static JSX Elements
|
|
642
|
-
|
|
643
|
-
Extract static JSX outside components to avoid re-creation.
|
|
644
|
-
|
|
645
|
-
**Incorrect (recreates element every render):**
|
|
646
|
-
|
|
647
|
-
```tsx
|
|
648
|
-
function LoadingSkeleton() {
|
|
649
|
-
return <div className="animate-pulse h-20 bg-gray-200" />
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function Container() {
|
|
653
|
-
return (
|
|
654
|
-
<div>
|
|
655
|
-
{loading && <LoadingSkeleton />}
|
|
656
|
-
</div>
|
|
657
|
-
)
|
|
658
|
-
}
|
|
659
|
-
```
|
|
660
|
-
|
|
661
|
-
**Correct (reuses same element):**
|
|
662
|
-
|
|
663
|
-
```tsx
|
|
664
|
-
const loadingSkeleton = (
|
|
665
|
-
<div className="animate-pulse h-20 bg-gray-200" />
|
|
666
|
-
)
|
|
667
|
-
|
|
668
|
-
function Container() {
|
|
669
|
-
return (
|
|
670
|
-
<div>
|
|
671
|
-
{loading && loadingSkeleton}
|
|
672
|
-
</div>
|
|
673
|
-
)
|
|
674
|
-
}
|
|
675
|
-
```
|
|
676
|
-
|
|
677
|
-
### 6.2 Use Activity Component for Show/Hide
|
|
678
|
-
|
|
679
|
-
Use React's `<Activity>` to preserve state/DOM for expensive
|
|
680
|
-
components that frequently toggle visibility.
|
|
681
|
-
|
|
682
|
-
**Usage:**
|
|
683
|
-
|
|
684
|
-
```tsx
|
|
685
|
-
import { Activity } from 'react'
|
|
686
|
-
|
|
687
|
-
function Dropdown({ isOpen }: Props) {
|
|
688
|
-
return (
|
|
689
|
-
<Activity mode={isOpen ? 'visible' : 'hidden'}>
|
|
690
|
-
<ExpensiveMenu />
|
|
691
|
-
</Activity>
|
|
692
|
-
)
|
|
693
|
-
}
|
|
694
|
-
```
|
|
695
|
-
|
|
696
|
-
### 6.3 CSS content-visibility for Long Lists
|
|
697
|
-
|
|
698
|
-
Apply `content-visibility: auto` to defer off-screen rendering.
|
|
699
|
-
|
|
700
|
-
**CSS:**
|
|
701
|
-
|
|
702
|
-
```css
|
|
703
|
-
.message-item {
|
|
704
|
-
content-visibility: auto;
|
|
705
|
-
contain-intrinsic-size: 0 80px;
|
|
706
|
-
}
|
|
707
|
-
```
|
|
708
|
-
|
|
709
|
-
**Example:**
|
|
710
|
-
|
|
711
|
-
```tsx
|
|
712
|
-
function MessageList({ messages }: { messages: Message[] }) {
|
|
713
|
-
return (
|
|
714
|
-
<div className="overflow-y-auto h-screen">
|
|
715
|
-
{messages.map(msg => (
|
|
716
|
-
<div key={msg.id} className="message-item">
|
|
717
|
-
<Avatar user={msg.author} />
|
|
718
|
-
<div>{msg.content}</div>
|
|
719
|
-
</div>
|
|
720
|
-
))}
|
|
721
|
-
</div>
|
|
722
|
-
)
|
|
723
|
-
}
|
|
724
|
-
```
|
|
725
|
-
|
|
726
|
-
For 1000 messages, browser skips layout/paint for ~990
|
|
727
|
-
off-screen items (10x faster initial render).
|
|
728
|
-
|
|
729
|
-
### 6.4 Optimize SVG Precision
|
|
730
|
-
|
|
731
|
-
Reduce SVG coordinate precision to decrease file size.
|
|
732
|
-
|
|
733
|
-
**Incorrect (excessive precision):**
|
|
734
|
-
|
|
735
|
-
```svg
|
|
736
|
-
<path d="M 10.293847 20.847362 L 30.938472 40.192837" />
|
|
737
|
-
```
|
|
738
|
-
|
|
739
|
-
**Correct (1 decimal place):**
|
|
740
|
-
|
|
741
|
-
```svg
|
|
742
|
-
<path d="M 10.3 20.8 L 30.9 40.2" />
|
|
743
|
-
```
|
|
744
|
-
|
|
745
|
-
**Automate with SVGO:**
|
|
746
|
-
|
|
747
|
-
```bash
|
|
748
|
-
npx svgo --precision=1 --multipass icon.svg
|
|
749
|
-
```
|
|
750
|
-
|
|
751
|
-
---
|
|
752
|
-
|
|
753
|
-
## 7. JavaScript Performance
|
|
754
|
-
|
|
755
|
-
**Impact: LOW-MEDIUM (micro-optimizations for hot paths)**
|
|
756
|
-
|
|
757
|
-
### 7.1 Combine Multiple Array Iterations
|
|
758
|
-
|
|
759
|
-
Multiple `.filter()` or `.map()` calls iterate the array
|
|
760
|
-
multiple times. Combine into one loop.
|
|
761
|
-
|
|
762
|
-
**Incorrect (3 iterations):**
|
|
763
|
-
|
|
764
|
-
```typescript
|
|
765
|
-
const admins = users.filter(u => u.isAdmin)
|
|
766
|
-
const testers = users.filter(u => u.isTester)
|
|
767
|
-
const inactive = users.filter(u => !u.isActive)
|
|
768
|
-
```
|
|
769
|
-
|
|
770
|
-
**Correct (1 iteration):**
|
|
771
|
-
|
|
772
|
-
```typescript
|
|
773
|
-
const admins: User[] = []
|
|
774
|
-
const testers: User[] = []
|
|
775
|
-
const inactive: User[] = []
|
|
776
|
-
|
|
777
|
-
for (const user of users) {
|
|
778
|
-
if (user.isAdmin) admins.push(user)
|
|
779
|
-
if (user.isTester) testers.push(user)
|
|
780
|
-
if (!user.isActive) inactive.push(user)
|
|
781
|
-
}
|
|
782
|
-
```
|
|
783
|
-
|
|
784
|
-
### 7.2 Cache Property Access in Loops
|
|
785
|
-
|
|
786
|
-
Cache object property lookups in hot paths.
|
|
787
|
-
|
|
788
|
-
**Incorrect (3 lookups x N iterations):**
|
|
789
|
-
|
|
790
|
-
```typescript
|
|
791
|
-
for (let i = 0; i < arr.length; i++) {
|
|
792
|
-
process(obj.config.settings.value)
|
|
793
|
-
}
|
|
794
|
-
```
|
|
795
|
-
|
|
796
|
-
**Correct (1 lookup total):**
|
|
797
|
-
|
|
798
|
-
```typescript
|
|
799
|
-
const value = obj.config.settings.value
|
|
800
|
-
const len = arr.length
|
|
801
|
-
for (let i = 0; i < len; i++) {
|
|
802
|
-
process(value)
|
|
803
|
-
}
|
|
804
|
-
```
|
|
805
|
-
|
|
806
|
-
### 7.3 Use Set/Map for O(1) Lookups
|
|
807
|
-
|
|
808
|
-
Convert arrays to Set/Map for repeated membership checks.
|
|
809
|
-
|
|
810
|
-
**Incorrect (O(n) per check):**
|
|
811
|
-
|
|
812
|
-
```typescript
|
|
813
|
-
const allowedIds = ['a', 'b', 'c', ...]
|
|
814
|
-
items.filter(item => allowedIds.includes(item.id))
|
|
815
|
-
```
|
|
816
|
-
|
|
817
|
-
**Correct (O(1) per check):**
|
|
818
|
-
|
|
819
|
-
```typescript
|
|
820
|
-
const allowedIds = new Set(['a', 'b', 'c', ...])
|
|
821
|
-
items.filter(item => allowedIds.has(item.id))
|
|
822
|
-
```
|
|
823
|
-
|
|
824
|
-
### 7.4 Early Exit from Loops
|
|
825
|
-
|
|
826
|
-
Exit as soon as result is determined.
|
|
827
|
-
|
|
828
|
-
**Incorrect (always iterates all):**
|
|
829
|
-
|
|
830
|
-
```typescript
|
|
831
|
-
const hasAdmin = users.some(u => u.role === 'admin')
|
|
832
|
-
const admin = users.find(u => u.role === 'admin')
|
|
833
|
-
// Two iterations
|
|
834
|
-
```
|
|
835
|
-
|
|
836
|
-
**Correct (single pass with early exit):**
|
|
837
|
-
|
|
838
|
-
```typescript
|
|
839
|
-
let admin: User | undefined
|
|
840
|
-
for (const user of users) {
|
|
841
|
-
if (user.role === 'admin') {
|
|
842
|
-
admin = user
|
|
843
|
-
break
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
const hasAdmin = admin !== undefined
|
|
847
|
-
```
|
|
848
|
-
|
|
849
|
-
### 7.5 Hoist RegExp Creation
|
|
850
|
-
|
|
851
|
-
Don't create RegExp inside render. Hoist to module scope or
|
|
852
|
-
memoize with `useMemo()`.
|
|
853
|
-
|
|
854
|
-
**Incorrect (new RegExp every render):**
|
|
855
|
-
|
|
856
|
-
```tsx
|
|
857
|
-
function Highlighter({ text, query }: Props) {
|
|
858
|
-
const regex = new RegExp(`(${query})`, 'gi')
|
|
859
|
-
const parts = text.split(regex)
|
|
860
|
-
return <>{parts.map((part, i) => ...)}</>
|
|
861
|
-
}
|
|
862
|
-
```
|
|
863
|
-
|
|
864
|
-
**Correct (memoize or hoist):**
|
|
865
|
-
|
|
866
|
-
```tsx
|
|
867
|
-
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
868
|
-
|
|
869
|
-
function Highlighter({ text, query }: Props) {
|
|
870
|
-
const regex = useMemo(
|
|
871
|
-
() => new RegExp(`(${escapeRegex(query)})`, 'gi'),
|
|
872
|
-
[query]
|
|
873
|
-
)
|
|
874
|
-
const parts = text.split(regex)
|
|
875
|
-
return <>{parts.map((part, i) => ...)}</>
|
|
876
|
-
}
|
|
877
|
-
```
|
|
878
|
-
|
|
879
|
-
### 7.6 Cache Storage API Calls
|
|
880
|
-
|
|
881
|
-
`localStorage`, `sessionStorage`, and `document.cookie` are
|
|
882
|
-
synchronous and expensive. Cache reads in memory.
|
|
883
|
-
|
|
884
|
-
**Incorrect (reads storage on every call):**
|
|
885
|
-
|
|
886
|
-
```typescript
|
|
887
|
-
function getTheme() {
|
|
888
|
-
return localStorage.getItem('theme') ?? 'light'
|
|
889
|
-
}
|
|
890
|
-
// Called 10 times = 10 storage reads
|
|
891
|
-
```
|
|
892
|
-
|
|
893
|
-
**Correct (Map cache):**
|
|
894
|
-
|
|
895
|
-
```typescript
|
|
896
|
-
const storageCache = new Map<string, string | null>()
|
|
897
|
-
|
|
898
|
-
function getLocalStorage(key: string) {
|
|
899
|
-
if (!storageCache.has(key)) {
|
|
900
|
-
storageCache.set(key, localStorage.getItem(key))
|
|
901
|
-
}
|
|
902
|
-
return storageCache.get(key)
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
function setLocalStorage(key: string, value: string) {
|
|
906
|
-
localStorage.setItem(key, value)
|
|
907
|
-
storageCache.set(key, value) // keep cache in sync
|
|
908
|
-
}
|
|
909
|
-
```
|
|
910
|
-
|
|
911
|
-
### 7.7 Build Index Maps for Repeated Lookups
|
|
912
|
-
|
|
913
|
-
Multiple `.find()` calls by the same key should use a Map.
|
|
914
|
-
|
|
915
|
-
**Incorrect (O(n) per lookup):**
|
|
916
|
-
|
|
917
|
-
```typescript
|
|
918
|
-
function processOrders(orders: Order[], users: User[]) {
|
|
919
|
-
return orders.map(order => ({
|
|
920
|
-
...order,
|
|
921
|
-
user: users.find(u => u.id === order.userId)
|
|
922
|
-
}))
|
|
923
|
-
}
|
|
924
|
-
```
|
|
925
|
-
|
|
926
|
-
**Correct (O(1) per lookup):**
|
|
927
|
-
|
|
928
|
-
```typescript
|
|
929
|
-
function processOrders(orders: Order[], users: User[]) {
|
|
930
|
-
const userById = new Map(users.map(u => [u.id, u]))
|
|
931
|
-
|
|
932
|
-
return orders.map(order => ({
|
|
933
|
-
...order,
|
|
934
|
-
user: userById.get(order.userId)
|
|
935
|
-
}))
|
|
936
|
-
}
|
|
937
|
-
```
|
|
938
|
-
|
|
939
|
-
Build map once (O(n)), then all lookups are O(1).
|
|
940
|
-
For 1000 orders x 1000 users: 1M ops -> 2K ops.
|
|
941
|
-
|
|
942
|
-
---
|
|
943
|
-
|
|
944
|
-
## 8. Advanced Patterns
|
|
945
|
-
|
|
946
|
-
**Impact: LOW (advanced patterns for specific cases)**
|
|
947
|
-
|
|
948
|
-
### 8.1 useLatest for Stable Callback Refs
|
|
949
|
-
|
|
950
|
-
Access latest values in callbacks without adding them to
|
|
951
|
-
dependency arrays. Prevents effect re-runs while avoiding
|
|
952
|
-
stale closures.
|
|
953
|
-
|
|
954
|
-
**Implementation:**
|
|
955
|
-
|
|
956
|
-
```typescript
|
|
957
|
-
function useLatest<T>(value: T) {
|
|
958
|
-
const ref = useRef(value)
|
|
959
|
-
useEffect(() => {
|
|
960
|
-
ref.current = value
|
|
961
|
-
}, [value])
|
|
962
|
-
return ref
|
|
963
|
-
}
|
|
964
|
-
```
|
|
965
|
-
|
|
966
|
-
**Incorrect (effect re-runs on every callback change):**
|
|
967
|
-
|
|
968
|
-
```tsx
|
|
969
|
-
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
|
970
|
-
const [query, setQuery] = useState('')
|
|
971
|
-
|
|
972
|
-
useEffect(() => {
|
|
973
|
-
const timeout = setTimeout(() => onSearch(query), 300)
|
|
974
|
-
return () => clearTimeout(timeout)
|
|
975
|
-
}, [query, onSearch])
|
|
976
|
-
}
|
|
977
|
-
```
|
|
978
|
-
|
|
979
|
-
**Correct (stable effect, fresh callback):**
|
|
980
|
-
|
|
981
|
-
```tsx
|
|
982
|
-
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
|
983
|
-
const [query, setQuery] = useState('')
|
|
984
|
-
const onSearchRef = useLatest(onSearch)
|
|
985
|
-
|
|
986
|
-
useEffect(() => {
|
|
987
|
-
const timeout = setTimeout(() => onSearchRef.current(query), 300)
|
|
988
|
-
return () => clearTimeout(timeout)
|
|
989
|
-
}, [query])
|
|
990
|
-
}
|
|
991
|
-
```
|
|
992
|
-
|
|
993
|
-
### 8.2 Store Event Handlers in Refs
|
|
994
|
-
|
|
995
|
-
Store callbacks in refs when used in effects that shouldn't
|
|
996
|
-
re-subscribe on callback changes.
|
|
997
|
-
|
|
998
|
-
**Incorrect (re-subscribes on every render):**
|
|
999
|
-
|
|
1000
|
-
```tsx
|
|
1001
|
-
function useWindowEvent(event: string, handler: () => void) {
|
|
1002
|
-
useEffect(() => {
|
|
1003
|
-
window.addEventListener(event, handler)
|
|
1004
|
-
return () => window.removeEventListener(event, handler)
|
|
1005
|
-
}, [event, handler])
|
|
1006
|
-
}
|
|
1007
|
-
```
|
|
1008
|
-
|
|
1009
|
-
**Correct (stable subscription):**
|
|
1010
|
-
|
|
1011
|
-
```tsx
|
|
1012
|
-
function useWindowEvent(event: string, handler: () => void) {
|
|
1013
|
-
const handlerRef = useRef(handler)
|
|
1014
|
-
useEffect(() => {
|
|
1015
|
-
handlerRef.current = handler
|
|
1016
|
-
}, [handler])
|
|
1017
|
-
|
|
1018
|
-
useEffect(() => {
|
|
1019
|
-
const listener = () => handlerRef.current()
|
|
1020
|
-
window.addEventListener(event, listener)
|
|
1021
|
-
return () => window.removeEventListener(event, listener)
|
|
1022
|
-
}, [event])
|
|
1023
|
-
}
|
|
1024
|
-
```
|
|
1025
|
-
|
|
1026
|
-
---
|
|
1027
|
-
|
|
1028
|
-
## References
|
|
1029
|
-
|
|
1030
|
-
1. [React Documentation](https://react.dev)
|
|
1031
|
-
2. [Next.js Documentation](https://nextjs.org)
|
|
1032
|
-
3. [SWR Documentation](https://swr.vercel.app)
|
|
1033
|
-
4. [better-all](https://github.com/shuding/better-all)
|
|
1034
|
-
5. [LRU Cache](https://github.com/isaacs/node-lru-cache)
|