red64-cli 0.1.0 → 0.3.0
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/README.md +1 -2
- package/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +5 -0
- package/dist/cli/parseArgs.js.map +1 -1
- package/dist/components/init/CompleteStep.d.ts.map +1 -1
- package/dist/components/init/CompleteStep.js +2 -2
- package/dist/components/init/CompleteStep.js.map +1 -1
- package/dist/components/init/TestCheckStep.d.ts +16 -0
- package/dist/components/init/TestCheckStep.d.ts.map +1 -0
- package/dist/components/init/TestCheckStep.js +120 -0
- package/dist/components/init/TestCheckStep.js.map +1 -0
- package/dist/components/init/index.d.ts +1 -0
- package/dist/components/init/index.d.ts.map +1 -1
- package/dist/components/init/index.js +1 -0
- package/dist/components/init/index.js.map +1 -1
- package/dist/components/init/types.d.ts +9 -0
- package/dist/components/init/types.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.js +69 -6
- package/dist/components/screens/InitScreen.js.map +1 -1
- package/dist/components/screens/ListScreen.d.ts.map +1 -1
- package/dist/components/screens/ListScreen.js +28 -3
- package/dist/components/screens/ListScreen.js.map +1 -1
- package/dist/components/screens/StartScreen.d.ts.map +1 -1
- package/dist/components/screens/StartScreen.js +212 -13
- package/dist/components/screens/StartScreen.js.map +1 -1
- package/dist/components/ui/ArtifactsSidebar.d.ts +19 -0
- package/dist/components/ui/ArtifactsSidebar.d.ts.map +1 -0
- package/dist/components/ui/ArtifactsSidebar.js +51 -0
- package/dist/components/ui/ArtifactsSidebar.js.map +1 -0
- package/dist/components/ui/FeatureSidebar.d.ts.map +1 -1
- package/dist/components/ui/FeatureSidebar.js +1 -1
- package/dist/components/ui/FeatureSidebar.js.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +1 -0
- package/dist/components/ui/index.js.map +1 -1
- package/dist/services/ClaudeErrorDetector.js +3 -3
- package/dist/services/ClaudeErrorDetector.js.map +1 -1
- package/dist/services/ConfigService.d.ts +1 -0
- package/dist/services/ConfigService.d.ts.map +1 -1
- package/dist/services/ConfigService.js.map +1 -1
- package/dist/services/ProjectDetector.d.ts +28 -0
- package/dist/services/ProjectDetector.d.ts.map +1 -0
- package/dist/services/ProjectDetector.js +236 -0
- package/dist/services/ProjectDetector.js.map +1 -0
- package/dist/services/TestRunner.d.ts +46 -0
- package/dist/services/TestRunner.d.ts.map +1 -0
- package/dist/services/TestRunner.js +85 -0
- package/dist/services/TestRunner.js.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -0
- package/dist/services/index.js.map +1 -1
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/framework/.red64/settings/templates/specs/gap-analysis.md +163 -0
- package/framework/agents/claude/.claude/agents/red64/spec-impl.md +131 -2
- package/framework/agents/claude/.claude/agents/red64/validate-gap.md +13 -7
- package/framework/agents/claude/.claude/commands/red64/spec-impl.md +24 -0
- package/framework/agents/claude/.claude/commands/red64/validate-gap.md +4 -0
- package/framework/agents/codex/.codex/agents/red64/spec-impl.md +131 -2
- package/framework/agents/codex/.codex/agents/red64/validate-gap.md +13 -7
- package/framework/agents/codex/.codex/commands/red64/spec-impl.md +24 -0
- package/framework/agents/codex/.codex/commands/red64/validate-gap.md +4 -0
- package/framework/stacks/generic/feedback.md +80 -0
- package/framework/stacks/nextjs/accessibility.md +437 -0
- package/framework/stacks/nextjs/api.md +431 -0
- package/framework/stacks/nextjs/coding-style.md +282 -0
- package/framework/stacks/nextjs/commenting.md +226 -0
- package/framework/stacks/nextjs/components.md +411 -0
- package/framework/stacks/nextjs/conventions.md +333 -0
- package/framework/stacks/nextjs/css.md +310 -0
- package/framework/stacks/nextjs/error-handling.md +442 -0
- package/framework/stacks/nextjs/feedback.md +124 -0
- package/framework/stacks/nextjs/migrations.md +332 -0
- package/framework/stacks/nextjs/models.md +362 -0
- package/framework/stacks/nextjs/queries.md +410 -0
- package/framework/stacks/nextjs/responsive.md +338 -0
- package/framework/stacks/nextjs/tech-stack.md +177 -0
- package/framework/stacks/nextjs/test-writing.md +475 -0
- package/framework/stacks/nextjs/validation.md +467 -0
- package/framework/stacks/python/api.md +468 -0
- package/framework/stacks/python/authentication.md +342 -0
- package/framework/stacks/python/code-quality.md +283 -0
- package/framework/stacks/python/code-refactoring.md +315 -0
- package/framework/stacks/python/coding-style.md +462 -0
- package/framework/stacks/python/conventions.md +399 -0
- package/framework/stacks/python/error-handling.md +512 -0
- package/framework/stacks/python/feedback.md +92 -0
- package/framework/stacks/python/implement-ai-llm.md +468 -0
- package/framework/stacks/python/migrations.md +388 -0
- package/framework/stacks/python/models.md +399 -0
- package/framework/stacks/python/python.md +232 -0
- package/framework/stacks/python/queries.md +451 -0
- package/framework/stacks/python/structure.md +245 -58
- package/framework/stacks/python/tech.md +92 -35
- package/framework/stacks/python/testing.md +380 -0
- package/framework/stacks/python/validation.md +471 -0
- package/framework/stacks/rails/authentication.md +176 -0
- package/framework/stacks/rails/code-quality.md +287 -0
- package/framework/stacks/rails/code-refactoring.md +299 -0
- package/framework/stacks/rails/feedback.md +130 -0
- package/framework/stacks/rails/implement-ai-llm-with-rubyllm.md +342 -0
- package/framework/stacks/rails/rails.md +301 -0
- package/framework/stacks/rails/rails8-best-practices.md +498 -0
- package/framework/stacks/rails/rails8-css.md +573 -0
- package/framework/stacks/rails/structure.md +140 -0
- package/framework/stacks/rails/tech.md +108 -0
- package/framework/stacks/react/code-quality.md +521 -0
- package/framework/stacks/react/components.md +625 -0
- package/framework/stacks/react/data-fetching.md +586 -0
- package/framework/stacks/react/feedback.md +110 -0
- package/framework/stacks/react/forms.md +694 -0
- package/framework/stacks/react/performance.md +640 -0
- package/framework/stacks/react/product.md +22 -9
- package/framework/stacks/react/state-management.md +472 -0
- package/framework/stacks/react/structure.md +351 -44
- package/framework/stacks/react/tech.md +219 -30
- package/framework/stacks/react/testing.md +690 -0
- package/package.json +1 -1
- package/framework/stacks/node/product.md +0 -27
- package/framework/stacks/node/structure.md +0 -82
- package/framework/stacks/node/tech.md +0 -63
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
# Performance Patterns
|
|
2
|
+
|
|
3
|
+
React performance optimization patterns for fast, responsive applications.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Measure first**: Don't optimize without profiling; intuition is often wrong
|
|
10
|
+
- **Optimize selectively**: Focus on actual bottlenecks, not hypothetical ones
|
|
11
|
+
- **User perception matters**: 100ms feels instant, 300ms feels responsive
|
|
12
|
+
- **Bundle size is performance**: Every KB counts on slow connections
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Performance Priorities
|
|
17
|
+
|
|
18
|
+
1. **Initial load time**: Time to First Contentful Paint (FCP)
|
|
19
|
+
2. **Time to Interactive (TTI)**: When users can interact
|
|
20
|
+
3. **Runtime performance**: Smooth 60fps interactions
|
|
21
|
+
4. **Memory usage**: Avoid leaks, minimize footprint
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Code Splitting
|
|
26
|
+
|
|
27
|
+
### Route-Level Splitting (Essential)
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// app/routes.tsx
|
|
31
|
+
import { lazy, Suspense } from 'react';
|
|
32
|
+
import { createBrowserRouter } from 'react-router-dom';
|
|
33
|
+
|
|
34
|
+
// Lazy load route components
|
|
35
|
+
const Dashboard = lazy(() => import('@/features/dashboard/pages/Dashboard'));
|
|
36
|
+
const Users = lazy(() => import('@/features/users/pages/Users'));
|
|
37
|
+
const Settings = lazy(() => import('@/features/settings/pages/Settings'));
|
|
38
|
+
|
|
39
|
+
// Loading fallback
|
|
40
|
+
function PageLoader() {
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex h-full items-center justify-center">
|
|
43
|
+
<Spinner size="lg" />
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const router = createBrowserRouter([
|
|
49
|
+
{
|
|
50
|
+
path: '/',
|
|
51
|
+
element: <Layout />,
|
|
52
|
+
children: [
|
|
53
|
+
{
|
|
54
|
+
path: 'dashboard',
|
|
55
|
+
element: (
|
|
56
|
+
<Suspense fallback={<PageLoader />}>
|
|
57
|
+
<Dashboard />
|
|
58
|
+
</Suspense>
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
path: 'users/*',
|
|
63
|
+
element: (
|
|
64
|
+
<Suspense fallback={<PageLoader />}>
|
|
65
|
+
<Users />
|
|
66
|
+
</Suspense>
|
|
67
|
+
),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
path: 'settings',
|
|
71
|
+
element: (
|
|
72
|
+
<Suspense fallback={<PageLoader />}>
|
|
73
|
+
<Settings />
|
|
74
|
+
</Suspense>
|
|
75
|
+
),
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
]);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Component-Level Splitting
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// Split heavy components
|
|
86
|
+
const Chart = lazy(() => import('@/components/Chart'));
|
|
87
|
+
const RichTextEditor = lazy(() => import('@/components/RichTextEditor'));
|
|
88
|
+
const CodeEditor = lazy(() => import('@/components/CodeEditor'));
|
|
89
|
+
|
|
90
|
+
function Dashboard() {
|
|
91
|
+
return (
|
|
92
|
+
<div>
|
|
93
|
+
<h1>Dashboard</h1>
|
|
94
|
+
<Suspense fallback={<ChartSkeleton />}>
|
|
95
|
+
<Chart data={data} />
|
|
96
|
+
</Suspense>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Named Exports with Lazy
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// For named exports, use intermediate module
|
|
106
|
+
// components/Chart/index.ts
|
|
107
|
+
export { Chart } from './Chart';
|
|
108
|
+
|
|
109
|
+
// Lazy import
|
|
110
|
+
const Chart = lazy(() =>
|
|
111
|
+
import('@/components/Chart').then((module) => ({ default: module.Chart }))
|
|
112
|
+
);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Memoization
|
|
118
|
+
|
|
119
|
+
### When to Use React.memo
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// Use memo when:
|
|
123
|
+
// 1. Component renders often with same props
|
|
124
|
+
// 2. Component is expensive to render
|
|
125
|
+
// 3. Parent re-renders frequently
|
|
126
|
+
|
|
127
|
+
// Good candidate: Pure display component rendered in a list
|
|
128
|
+
const UserCard = memo(function UserCard({ user }: { user: User }) {
|
|
129
|
+
return (
|
|
130
|
+
<div className="user-card">
|
|
131
|
+
<Avatar src={user.avatar} alt={user.name} />
|
|
132
|
+
<h3>{user.name}</h3>
|
|
133
|
+
<p>{user.email}</p>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Bad candidate: Simple component, renders rarely
|
|
139
|
+
// Don't memo this - overhead not worth it
|
|
140
|
+
function PageTitle({ title }: { title: string }) {
|
|
141
|
+
return <h1>{title}</h1>;
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### useMemo for Expensive Computations
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
function DataTable({ data, sortBy, filters }: Props) {
|
|
149
|
+
// Expensive: filtering and sorting large dataset
|
|
150
|
+
const processedData = useMemo(() => {
|
|
151
|
+
let result = [...data];
|
|
152
|
+
|
|
153
|
+
// Filter
|
|
154
|
+
if (filters.status) {
|
|
155
|
+
result = result.filter((item) => item.status === filters.status);
|
|
156
|
+
}
|
|
157
|
+
if (filters.search) {
|
|
158
|
+
result = result.filter((item) =>
|
|
159
|
+
item.name.toLowerCase().includes(filters.search.toLowerCase())
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Sort
|
|
164
|
+
result.sort((a, b) => {
|
|
165
|
+
const aVal = a[sortBy.field];
|
|
166
|
+
const bVal = b[sortBy.field];
|
|
167
|
+
return sortBy.direction === 'asc'
|
|
168
|
+
? aVal.localeCompare(bVal)
|
|
169
|
+
: bVal.localeCompare(aVal);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return result;
|
|
173
|
+
}, [data, sortBy, filters]); // Only recompute when these change
|
|
174
|
+
|
|
175
|
+
return <Table data={processedData} />;
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### useCallback for Stable References
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
function ParentComponent() {
|
|
183
|
+
const [items, setItems] = useState<Item[]>([]);
|
|
184
|
+
|
|
185
|
+
// Without useCallback: new function every render
|
|
186
|
+
// Children with memo would still re-render
|
|
187
|
+
const handleDelete = useCallback((id: string) => {
|
|
188
|
+
setItems((prev) => prev.filter((item) => item.id !== id));
|
|
189
|
+
}, []); // Empty deps: function never changes
|
|
190
|
+
|
|
191
|
+
const handleUpdate = useCallback((id: string, data: Partial<Item>) => {
|
|
192
|
+
setItems((prev) =>
|
|
193
|
+
prev.map((item) => (item.id === id ? { ...item, ...data } : item))
|
|
194
|
+
);
|
|
195
|
+
}, []);
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<div>
|
|
199
|
+
{items.map((item) => (
|
|
200
|
+
<MemoizedItem
|
|
201
|
+
key={item.id}
|
|
202
|
+
item={item}
|
|
203
|
+
onDelete={handleDelete}
|
|
204
|
+
onUpdate={handleUpdate}
|
|
205
|
+
/>
|
|
206
|
+
))}
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### When NOT to Memoize
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// Don't memoize:
|
|
216
|
+
|
|
217
|
+
// 1. Simple/cheap computations
|
|
218
|
+
const fullName = `${user.firstName} ${user.lastName}`; // Just string concat
|
|
219
|
+
|
|
220
|
+
// 2. Primitives that change every render anyway
|
|
221
|
+
const now = new Date(); // Always new
|
|
222
|
+
|
|
223
|
+
// 3. Components that always receive new props
|
|
224
|
+
// If parent always creates new objects, memo is useless
|
|
225
|
+
<Child data={{ name: 'John' }} /> // New object every render
|
|
226
|
+
|
|
227
|
+
// 4. Components rendered once
|
|
228
|
+
function App() {
|
|
229
|
+
return <Layout />; // Only renders once
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Virtualization
|
|
236
|
+
|
|
237
|
+
### Long Lists with @tanstack/react-virtual
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
241
|
+
|
|
242
|
+
function VirtualList({ items }: { items: Item[] }) {
|
|
243
|
+
const parentRef = useRef<HTMLDivElement>(null);
|
|
244
|
+
|
|
245
|
+
const virtualizer = useVirtualizer({
|
|
246
|
+
count: items.length,
|
|
247
|
+
getScrollElement: () => parentRef.current,
|
|
248
|
+
estimateSize: () => 50, // Estimated row height
|
|
249
|
+
overscan: 5, // Render 5 extra items above/below viewport
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<div
|
|
254
|
+
ref={parentRef}
|
|
255
|
+
className="h-[400px] overflow-auto"
|
|
256
|
+
>
|
|
257
|
+
<div
|
|
258
|
+
style={{
|
|
259
|
+
height: `${virtualizer.getTotalSize()}px`,
|
|
260
|
+
position: 'relative',
|
|
261
|
+
}}
|
|
262
|
+
>
|
|
263
|
+
{virtualizer.getVirtualItems().map((virtualItem) => (
|
|
264
|
+
<div
|
|
265
|
+
key={virtualItem.key}
|
|
266
|
+
style={{
|
|
267
|
+
position: 'absolute',
|
|
268
|
+
top: 0,
|
|
269
|
+
left: 0,
|
|
270
|
+
width: '100%',
|
|
271
|
+
height: `${virtualItem.size}px`,
|
|
272
|
+
transform: `translateY(${virtualItem.start}px)`,
|
|
273
|
+
}}
|
|
274
|
+
>
|
|
275
|
+
<ListItem item={items[virtualItem.index]} />
|
|
276
|
+
</div>
|
|
277
|
+
))}
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### When to Virtualize
|
|
285
|
+
|
|
286
|
+
| List Size | Recommendation |
|
|
287
|
+
|-----------|----------------|
|
|
288
|
+
| < 100 items | No virtualization needed |
|
|
289
|
+
| 100-500 items | Consider if items are complex |
|
|
290
|
+
| 500+ items | Definitely virtualize |
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Image Optimization
|
|
295
|
+
|
|
296
|
+
### Lazy Loading Images
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
// Native lazy loading (modern browsers)
|
|
300
|
+
<img src={url} alt={alt} loading="lazy" />
|
|
301
|
+
|
|
302
|
+
// With intersection observer for more control
|
|
303
|
+
function LazyImage({ src, alt, ...props }: ImgProps) {
|
|
304
|
+
const [isLoaded, setIsLoaded] = useState(false);
|
|
305
|
+
const [isInView, setIsInView] = useState(false);
|
|
306
|
+
const imgRef = useRef<HTMLImageElement>(null);
|
|
307
|
+
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
const observer = new IntersectionObserver(
|
|
310
|
+
([entry]) => {
|
|
311
|
+
if (entry.isIntersecting) {
|
|
312
|
+
setIsInView(true);
|
|
313
|
+
observer.disconnect();
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
{ rootMargin: '200px' } // Load 200px before in view
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
if (imgRef.current) {
|
|
320
|
+
observer.observe(imgRef.current);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return () => observer.disconnect();
|
|
324
|
+
}, []);
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<div ref={imgRef} className="relative">
|
|
328
|
+
{isInView && (
|
|
329
|
+
<img
|
|
330
|
+
src={src}
|
|
331
|
+
alt={alt}
|
|
332
|
+
onLoad={() => setIsLoaded(true)}
|
|
333
|
+
className={cn('transition-opacity', isLoaded ? 'opacity-100' : 'opacity-0')}
|
|
334
|
+
{...props}
|
|
335
|
+
/>
|
|
336
|
+
)}
|
|
337
|
+
{!isLoaded && <Skeleton className="absolute inset-0" />}
|
|
338
|
+
</div>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Responsive Images
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
<picture>
|
|
347
|
+
<source
|
|
348
|
+
srcSet="/image-400.webp 400w, /image-800.webp 800w, /image-1200.webp 1200w"
|
|
349
|
+
type="image/webp"
|
|
350
|
+
sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
|
|
351
|
+
/>
|
|
352
|
+
<img
|
|
353
|
+
src="/image-800.jpg"
|
|
354
|
+
alt="Description"
|
|
355
|
+
loading="lazy"
|
|
356
|
+
decoding="async"
|
|
357
|
+
/>
|
|
358
|
+
</picture>
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Debouncing and Throttling
|
|
364
|
+
|
|
365
|
+
### Debounce Search Input
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
|
|
369
|
+
|
|
370
|
+
function SearchInput() {
|
|
371
|
+
const [query, setQuery] = useState('');
|
|
372
|
+
const debouncedQuery = useDebouncedValue(query, 300);
|
|
373
|
+
|
|
374
|
+
// API call uses debounced value
|
|
375
|
+
const { data } = useSearch(debouncedQuery);
|
|
376
|
+
|
|
377
|
+
return (
|
|
378
|
+
<input
|
|
379
|
+
value={query}
|
|
380
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
381
|
+
placeholder="Search..."
|
|
382
|
+
/>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// hooks/useDebouncedValue.ts
|
|
387
|
+
export function useDebouncedValue<T>(value: T, delay: number): T {
|
|
388
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
389
|
+
|
|
390
|
+
useEffect(() => {
|
|
391
|
+
const timer = setTimeout(() => setDebouncedValue(value), delay);
|
|
392
|
+
return () => clearTimeout(timer);
|
|
393
|
+
}, [value, delay]);
|
|
394
|
+
|
|
395
|
+
return debouncedValue;
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Throttle Scroll Handler
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
import { useThrottle } from '@/hooks/useThrottle';
|
|
403
|
+
|
|
404
|
+
function ScrollTracker() {
|
|
405
|
+
const [scrollY, setScrollY] = useState(0);
|
|
406
|
+
const throttledScrollY = useThrottle(scrollY, 100);
|
|
407
|
+
|
|
408
|
+
useEffect(() => {
|
|
409
|
+
const handleScroll = () => setScrollY(window.scrollY);
|
|
410
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
411
|
+
return () => window.removeEventListener('scroll', handleScroll);
|
|
412
|
+
}, []);
|
|
413
|
+
|
|
414
|
+
// Use throttledScrollY for expensive operations
|
|
415
|
+
useEffect(() => {
|
|
416
|
+
// This runs at most every 100ms
|
|
417
|
+
trackScrollPosition(throttledScrollY);
|
|
418
|
+
}, [throttledScrollY]);
|
|
419
|
+
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## State Updates Optimization
|
|
427
|
+
|
|
428
|
+
### Batch Updates
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
// React 18 batches by default in event handlers and effects
|
|
432
|
+
// But for async operations, use flushSync if needed
|
|
433
|
+
|
|
434
|
+
import { flushSync } from 'react-dom';
|
|
435
|
+
|
|
436
|
+
// Normally batched (good)
|
|
437
|
+
function handleClick() {
|
|
438
|
+
setCount((c) => c + 1);
|
|
439
|
+
setFlag((f) => !f);
|
|
440
|
+
// React renders once
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Manual batching for legacy code or specific needs
|
|
444
|
+
function handleAsync() {
|
|
445
|
+
fetchData().then(() => {
|
|
446
|
+
// React 18: Already batched
|
|
447
|
+
setData(data);
|
|
448
|
+
setLoading(false);
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### Avoid State Object Spreads in Loops
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
// BAD: Creates new object for each item
|
|
457
|
+
items.forEach((item) => {
|
|
458
|
+
setState((prev) => ({ ...prev, [item.id]: item })); // N state updates
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// GOOD: Single update with all changes
|
|
462
|
+
setState((prev) => {
|
|
463
|
+
const updates: Record<string, Item> = {};
|
|
464
|
+
items.forEach((item) => {
|
|
465
|
+
updates[item.id] = item;
|
|
466
|
+
});
|
|
467
|
+
return { ...prev, ...updates }; // 1 state update
|
|
468
|
+
});
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
## Bundle Size Optimization
|
|
474
|
+
|
|
475
|
+
### Import Only What You Need
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
// BAD: Imports entire library
|
|
479
|
+
import _ from 'lodash';
|
|
480
|
+
_.debounce(fn, 300);
|
|
481
|
+
|
|
482
|
+
// GOOD: Import specific function
|
|
483
|
+
import debounce from 'lodash/debounce';
|
|
484
|
+
debounce(fn, 300);
|
|
485
|
+
|
|
486
|
+
// BEST: Use native or lighter alternative
|
|
487
|
+
function debounce(fn: Function, ms: number) {
|
|
488
|
+
let timeout: NodeJS.Timeout;
|
|
489
|
+
return (...args: unknown[]) => {
|
|
490
|
+
clearTimeout(timeout);
|
|
491
|
+
timeout = setTimeout(() => fn(...args), ms);
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Analyze Bundle
|
|
497
|
+
|
|
498
|
+
```bash
|
|
499
|
+
# Vite bundle analyzer
|
|
500
|
+
pnpm add -D rollup-plugin-visualizer
|
|
501
|
+
|
|
502
|
+
# vite.config.ts
|
|
503
|
+
import { visualizer } from 'rollup-plugin-visualizer';
|
|
504
|
+
|
|
505
|
+
export default defineConfig({
|
|
506
|
+
plugins: [
|
|
507
|
+
react(),
|
|
508
|
+
visualizer({
|
|
509
|
+
filename: 'dist/stats.html',
|
|
510
|
+
open: true,
|
|
511
|
+
gzipSize: true,
|
|
512
|
+
}),
|
|
513
|
+
],
|
|
514
|
+
});
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
### Tree Shaking
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
// Ensure library supports tree shaking (ESM)
|
|
521
|
+
// Check package.json for "module" or "exports" field
|
|
522
|
+
|
|
523
|
+
// Named exports tree-shake better than default
|
|
524
|
+
export { Button } from './Button';
|
|
525
|
+
export { Input } from './Input';
|
|
526
|
+
// vs
|
|
527
|
+
export default { Button, Input }; // Harder to tree-shake
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## Profiling
|
|
533
|
+
|
|
534
|
+
### React DevTools Profiler
|
|
535
|
+
|
|
536
|
+
1. Open React DevTools → Profiler tab
|
|
537
|
+
2. Click Record
|
|
538
|
+
3. Perform the slow action
|
|
539
|
+
4. Click Stop
|
|
540
|
+
5. Analyze flame graph and ranked chart
|
|
541
|
+
|
|
542
|
+
### Performance API
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
// Measure component render time
|
|
546
|
+
function ExpensiveComponent() {
|
|
547
|
+
useEffect(() => {
|
|
548
|
+
performance.mark('expensive-start');
|
|
549
|
+
|
|
550
|
+
return () => {
|
|
551
|
+
performance.mark('expensive-end');
|
|
552
|
+
performance.measure('expensive-render', 'expensive-start', 'expensive-end');
|
|
553
|
+
const measure = performance.getEntriesByName('expensive-render')[0];
|
|
554
|
+
console.log(`Render took ${measure.duration}ms`);
|
|
555
|
+
};
|
|
556
|
+
}, []);
|
|
557
|
+
|
|
558
|
+
return <div>...</div>;
|
|
559
|
+
}
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### Why Did You Render (Development)
|
|
563
|
+
|
|
564
|
+
```typescript
|
|
565
|
+
// Install: pnpm add -D @welldone-software/why-did-you-render
|
|
566
|
+
|
|
567
|
+
// src/wdyr.ts
|
|
568
|
+
import React from 'react';
|
|
569
|
+
|
|
570
|
+
if (process.env.NODE_ENV === 'development') {
|
|
571
|
+
const whyDidYouRender = require('@welldone-software/why-did-you-render');
|
|
572
|
+
whyDidYouRender(React, {
|
|
573
|
+
trackAllPureComponents: true,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Import before React in main.tsx
|
|
578
|
+
import './wdyr';
|
|
579
|
+
import React from 'react';
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
---
|
|
583
|
+
|
|
584
|
+
## Core Web Vitals
|
|
585
|
+
|
|
586
|
+
| Metric | Target | Impact |
|
|
587
|
+
|--------|--------|--------|
|
|
588
|
+
| **LCP** (Largest Contentful Paint) | < 2.5s | Load performance |
|
|
589
|
+
| **FID** (First Input Delay) | < 100ms | Interactivity |
|
|
590
|
+
| **CLS** (Cumulative Layout Shift) | < 0.1 | Visual stability |
|
|
591
|
+
| **INP** (Interaction to Next Paint) | < 200ms | Responsiveness |
|
|
592
|
+
|
|
593
|
+
### Avoid CLS
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
// Reserve space for images
|
|
597
|
+
<img src={url} alt={alt} width={400} height={300} />
|
|
598
|
+
|
|
599
|
+
// Or use aspect ratio
|
|
600
|
+
<div className="aspect-video">
|
|
601
|
+
<img src={url} alt={alt} className="h-full w-full object-cover" />
|
|
602
|
+
</div>
|
|
603
|
+
|
|
604
|
+
// Skeleton for async content
|
|
605
|
+
{isLoading ? (
|
|
606
|
+
<Skeleton className="h-40 w-full" />
|
|
607
|
+
) : (
|
|
608
|
+
<Content data={data} />
|
|
609
|
+
)}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
## Anti-Patterns
|
|
615
|
+
|
|
616
|
+
| Anti-Pattern | Problem | Correct Approach |
|
|
617
|
+
|--------------|---------|------------------|
|
|
618
|
+
| Premature optimization | Wasted effort, complexity | Profile first, optimize bottlenecks |
|
|
619
|
+
| Memo everything | Memory overhead, complexity | Memo only expensive components |
|
|
620
|
+
| Inline objects in JSX | New reference every render | useMemo or extract to variable |
|
|
621
|
+
| State in parent for all | Unnecessary re-renders | Colocate state, use Zustand selectors |
|
|
622
|
+
| Giant components | Can't optimize parts | Split into smaller components |
|
|
623
|
+
| No loading states | Layout shift, bad UX | Use Suspense, skeletons |
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
## Quick Wins Checklist
|
|
628
|
+
|
|
629
|
+
- [ ] Route-level code splitting with React.lazy
|
|
630
|
+
- [ ] Images: loading="lazy", width/height attributes
|
|
631
|
+
- [ ] Virtualize lists > 100 items
|
|
632
|
+
- [ ] Debounce search/filter inputs (300ms)
|
|
633
|
+
- [ ] Use Zustand selectors (not full store)
|
|
634
|
+
- [ ] Production build with minification
|
|
635
|
+
- [ ] Bundle analyzer check for bloat
|
|
636
|
+
- [ ] Remove console.logs in production
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
_Performance is a feature. Measure, optimize the bottlenecks, and ship fast._
|
|
@@ -6,22 +6,35 @@
|
|
|
6
6
|
|
|
7
7
|
## Core Capabilities
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Define 3-5 key capabilities of your React application:
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
11
|
+
- **Interactive UI**: Rich, responsive user interfaces with real-time feedback
|
|
12
|
+
- **Client-Side State**: Efficient state management for complex user workflows
|
|
13
|
+
- **Data Synchronization**: Seamless API integration with optimistic updates
|
|
14
|
+
- **Accessible Experience**: WCAG-compliant interfaces for all users
|
|
14
15
|
|
|
15
16
|
## Target Use Cases
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
1. **Single Page Applications**: Complex client-side routing and state
|
|
19
|
+
2. **Dashboard & Admin Panels**: Data-heavy interfaces with real-time updates
|
|
20
|
+
3. **Forms & Workflows**: Multi-step processes with validation
|
|
21
|
+
4. **Real-Time Features**: Live updates, notifications, collaborative editing
|
|
21
22
|
|
|
22
23
|
## Value Proposition
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
What makes this React application unique:
|
|
26
|
+
|
|
27
|
+
- **Performance**: Vite for instant HMR, optimized production builds
|
|
28
|
+
- **Type Safety**: Full TypeScript coverage prevents runtime errors
|
|
29
|
+
- **Developer Experience**: Modern tooling with fast feedback loops
|
|
30
|
+
- **Maintainability**: Component-based architecture with clear patterns
|
|
31
|
+
|
|
32
|
+
## Success Metrics
|
|
33
|
+
|
|
34
|
+
- **Core Web Vitals**: LCP < 2.5s, FID < 100ms, CLS < 0.1
|
|
35
|
+
- **Test Coverage**: 80%+ for business logic
|
|
36
|
+
- **Bundle Size**: Initial load < 200KB gzipped
|
|
37
|
+
- **Accessibility**: Zero critical a11y violations
|
|
25
38
|
|
|
26
39
|
---
|
|
27
40
|
_Focus on patterns and purpose, not exhaustive feature lists_
|