picasso-skill 2.5.0 → 2.6.1

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.
@@ -1,6 +1,6 @@
1
1
  # Performance Optimization Reference
2
2
 
3
- React/Next.js performance rules based on Vercel's guidance. Organized by priority. Every pattern includes the why, the fix, and working code.
3
+ React/Next.js performance rules based on Vercel's guidance. Organized by priority.
4
4
 
5
5
  ---
6
6
 
@@ -8,728 +8,92 @@ React/Next.js performance rules based on Vercel's guidance. Organized by priorit
8
8
 
9
9
  Waterfalls are the single biggest performance killer. Every sequential `await` that could be parallel is wasted time.
10
10
 
11
- ### Promise.all() for Independent Operations
12
-
13
- ```typescript
14
- // BAD: Sequential - total time = A + B + C
15
- async function getPageData(userId: string) {
16
- const user = await fetchUser(userId);
17
- const posts = await fetchPosts(userId);
18
- const notifications = await fetchNotifications(userId);
19
- return { user, posts, notifications };
20
- }
21
-
22
- // GOOD: Parallel - total time = max(A, B, C)
23
- async function getPageData(userId: string) {
24
- const [user, posts, notifications] = await Promise.all([
25
- fetchUser(userId),
26
- fetchPosts(userId),
27
- fetchNotifications(userId),
28
- ]);
29
- return { user, posts, notifications };
30
- }
31
- ```
32
-
33
- ### Defer Await Until Needed
34
-
35
- Don't block code branches that don't use the data.
36
-
37
- ```typescript
38
- // BAD: Blocks even if we early-return
39
- async function handleRequest(req: Request) {
40
- const config = await fetchConfig();
41
- const user = await getUser(req);
42
-
43
- if (!user) {
44
- return redirect('/login'); // config was fetched for nothing
45
- }
46
-
47
- return renderPage(user, config);
48
- }
49
-
50
- // GOOD: Start fetch immediately, await only when needed
51
- async function handleRequest(req: Request) {
52
- const configPromise = fetchConfig(); // fire immediately, don't await
53
- const user = await getUser(req);
54
-
55
- if (!user) {
56
- return redirect('/login'); // config fetch may still be in-flight, that's fine
57
- }
58
-
59
- const config = await configPromise; // await only when we need it
60
- return renderPage(user, config);
61
- }
62
- ```
63
-
64
- ### Dependency-Based Parallelization with better-all
65
-
66
- When some fetches depend on others but you still want maximum parallelism:
67
-
68
- ```typescript
69
- import { all } from 'better-all';
70
-
71
- // Runs A and B in parallel. C starts as soon as A finishes (doesn't wait for B).
72
- const { user, posts, comments } = await all({
73
- user: () => fetchUser(id),
74
- posts: () => fetchPosts(id),
75
- comments: async ({ user }) => fetchComments(user.teamId), // depends on user
76
- });
77
- ```
78
-
79
- ### Strategic Suspense Boundaries
80
-
81
- Wrap independent data-loading sections in their own Suspense boundary so they stream independently.
82
-
83
- ```tsx
84
- // BAD: One boundary = entire page waits for slowest component
85
- <Suspense fallback={<PageSkeleton />}>
86
- <Header /> {/* 50ms */}
87
- <Feed /> {/* 2000ms - blocks everything */}
88
- <Sidebar /> {/* 100ms */}
89
- </Suspense>
90
-
91
- // GOOD: Independent boundaries = each streams when ready
92
- <Header />
93
- <Suspense fallback={<FeedSkeleton />}>
94
- <Feed />
95
- </Suspense>
96
- <Suspense fallback={<SidebarSkeleton />}>
97
- <Sidebar />
98
- </Suspense>
99
- ```
11
+ ### Rules
12
+ - Use `Promise.all()` for independent fetches (total time = max instead of sum)
13
+ - Start fetches immediately, defer `await` until the value is needed (fire-and-forget pattern)
14
+ - Use `better-all` when some fetches depend on others but you still want maximum parallelism
15
+ - Wrap independent data-loading sections in their own `<Suspense>` boundary so they stream independently (don't let one slow component block the whole page)
100
16
 
101
17
  ---
102
18
 
103
19
  ## Priority 2 - CRITICAL: Bundle Size
104
20
 
105
21
  ### Avoid Barrel File Imports
106
-
107
22
  Barrel files (`index.ts` re-exports) pull in entire modules. Cost: 200-800ms of parse time.
108
23
 
109
24
  ```typescript
110
25
  // BAD: Imports entire icon library through barrel file
111
26
  import { ChevronDown } from '@/components/icons';
112
- // This imports index.ts which re-exports 500 icons
113
-
114
27
  // GOOD: Direct import from source file
115
28
  import { ChevronDown } from '@/components/icons/ChevronDown';
116
-
117
- // BAD: Barrel file for utils
118
- import { formatDate } from '@/lib/utils';
119
- // Pulls in every util function
120
-
121
- // GOOD: Direct import
122
- import { formatDate } from '@/lib/utils/formatDate';
123
- ```
124
-
125
- Configure your bundler to detect this:
126
-
127
- ```javascript
128
- // next.config.js
129
- module.exports = {
130
- experimental: {
131
- optimizePackageImports: ['@/components/icons', 'lucide-react', 'date-fns'],
132
- },
133
- };
134
29
  ```
135
30
 
136
- ### Dynamic Imports for Heavy Components
137
-
138
- ```typescript
139
- import dynamic from 'next/dynamic';
140
-
141
- // Heavy editor component - only loads when rendered
142
- const CodeEditor = dynamic(() => import('@/components/CodeEditor'), {
143
- loading: () => <EditorSkeleton />,
144
- });
31
+ Configure detection: `optimizePackageImports` in `next.config.js` for known barrel-heavy packages.
145
32
 
146
- // Chart library - loads on demand
147
- const Chart = dynamic(() => import('@/components/Chart'), {
148
- loading: () => <ChartSkeleton />,
149
- ssr: false, // Skip server rendering for client-only libs
150
- });
151
- ```
152
-
153
- ### Defer Non-Critical Third-Party Scripts
154
-
155
- ```typescript
156
- // Analytics, error tracking, chat widgets - none are needed for first render
157
- const Analytics = dynamic(() => import('@/components/Analytics'), {
158
- ssr: false,
159
- });
160
- const ErrorTracker = dynamic(() => import('@/components/ErrorTracker'), {
161
- ssr: false,
162
- });
163
-
164
- // Load after page is interactive
165
- export default function Layout({ children }) {
166
- return (
167
- <>
168
- {children}
169
- <Suspense fallback={null}>
170
- <Analytics />
171
- <ErrorTracker />
172
- </Suspense>
173
- </>
174
- );
175
- }
176
- ```
33
+ ### Dynamic Imports
34
+ - Use `next/dynamic` for heavy components (editors, charts, maps) with `loading` fallback
35
+ - Use `ssr: false` for client-only libraries
36
+ - Defer non-critical third-party scripts (analytics, error tracking, chat widgets) via dynamic import + `<Suspense fallback={null}>`
177
37
 
178
38
  ### Preload on User Intent
179
-
180
- Start loading a route or component when the user shows intent (hover, focus) instead of on click.
181
-
182
- ```tsx
183
- import { useRouter } from 'next/navigation';
184
-
185
- function NavLink({ href, children }) {
186
- const router = useRouter();
187
-
188
- return (
189
- <Link
190
- href={href}
191
- onMouseEnter={() => router.prefetch(href)}
192
- onFocus={() => router.prefetch(href)}
193
- >
194
- {children}
195
- </Link>
196
- );
197
- }
198
- ```
199
-
200
- For heavy components:
201
-
202
- ```typescript
203
- // Preload the module on hover, render on click
204
- const importEditor = () => import('@/components/CodeEditor');
205
- const CodeEditor = dynamic(importEditor);
206
-
207
- function EditorButton() {
208
- const [show, setShow] = useState(false);
209
-
210
- return (
211
- <>
212
- <button
213
- onMouseEnter={() => importEditor()} // preload on hover
214
- onClick={() => setShow(true)}
215
- >
216
- Open Editor
217
- </button>
218
- {show && <CodeEditor />}
219
- </>
220
- );
221
- }
222
- ```
39
+ Start loading a route or component on hover/focus instead of on click. Use `router.prefetch(href)` on `onMouseEnter`/`onFocus`. For heavy components, call the dynamic import function on hover, render on click.
223
40
 
224
41
  ### Conditional Module Loading
225
-
226
- Only load modules when the feature is actually used.
227
-
228
- ```typescript
229
- async function exportData(format: 'csv' | 'xlsx') {
230
- if (format === 'xlsx') {
231
- // xlsx library is 200KB+ - only load when needed
232
- const XLSX = await import('xlsx');
233
- return XLSX.utils.json_to_sheet(data);
234
- }
235
- // CSV is trivial, no import needed
236
- return data.map(row => row.join(',')).join('\n');
237
- }
238
- ```
42
+ Only `import()` heavy modules when the feature path is actually taken (e.g., load `xlsx` only when user picks XLSX format).
239
43
 
240
44
  ---
241
45
 
242
46
  ## Priority 3 - HIGH: Server-Side Performance
243
47
 
244
- ### React.cache() for Per-Request Deduplication
245
-
246
- Multiple components in the same render can call the same function. `React.cache()` ensures it only executes once per request.
247
-
248
- ```typescript
249
- import { cache } from 'react';
250
-
251
- export const getUser = cache(async (userId: string) => {
252
- const res = await fetch(`/api/users/${userId}`);
253
- return res.json();
254
- });
255
-
256
- // Component A calls getUser('123') - makes network request
257
- // Component B calls getUser('123') - returns cached result from same request
258
- // Next request: cache is cleared, fresh fetch
259
- ```
260
-
261
- ### LRU Cache for Cross-Request Caching
262
-
263
- For data that doesn't change per-request (config, feature flags, static content):
264
-
265
- ```typescript
266
- import { LRUCache } from 'lru-cache';
267
-
268
- const cache = new LRUCache<string, any>({
269
- max: 500, // max entries
270
- ttl: 1000 * 60 * 5, // 5 minute TTL
271
- });
272
-
273
- export async function getConfig(key: string) {
274
- const cached = cache.get(key);
275
- if (cached) return cached;
276
-
277
- const config = await db.config.findUnique({ where: { key } });
278
- cache.set(key, config);
279
- return config;
280
- }
281
- ```
282
-
283
- ### Parallel Data Fetching with Component Composition
284
-
285
- Let each Server Component fetch its own data. React deduplicates and parallelizes automatically.
286
-
287
- ```tsx
288
- // page.tsx - Don't fetch everything here and pass down
289
- export default function DashboardPage() {
290
- return (
291
- <div>
292
- <UserHeader /> {/* fetches user data */}
293
- <StatsPanel /> {/* fetches stats data */}
294
- <ActivityFeed /> {/* fetches activity data */}
295
- </div>
296
- );
297
- }
298
-
299
- // Each component is independent - React runs them in parallel
300
- async function UserHeader() {
301
- const user = await getUser(); // deduplicated with React.cache
302
- return <header>{user.name}</header>;
303
- }
304
-
305
- async function StatsPanel() {
306
- const stats = await getStats();
307
- return <div>{stats.total}</div>;
308
- }
309
- ```
310
-
311
- ### Minimize Serialization at RSC Boundaries
312
-
313
- Only pass serializable, minimal data from Server to Client Components.
314
-
315
- ```tsx
316
- // BAD: Passing entire user object (large, may have non-serializable fields)
317
- <ClientComponent user={fullUserObject} />
318
-
319
- // GOOD: Pass only what the client needs
320
- <ClientComponent userName={user.name} userAvatar={user.avatarUrl} />
321
- ```
48
+ ### Rules
49
+ - Use `React.cache()` for per-request deduplication (multiple components calling the same function = one execution per request)
50
+ - Use `LRUCache` for cross-request caching (config, feature flags, static content) with TTL
51
+ - Let each Server Component fetch its own data; React deduplicates and parallelizes automatically
52
+ - Minimize serialization at RSC boundaries: pass only primitive/minimal data from Server to Client Components
322
53
 
323
54
  ---
324
55
 
325
56
  ## Priority 4 - MEDIUM-HIGH: Client-Side Data
326
57
 
327
- ### SWR for Deduplication, Caching, and Revalidation
328
-
329
- ```typescript
330
- import useSWR from 'swr';
331
-
332
- const fetcher = (url: string) => fetch(url).then(r => r.json());
333
-
334
- function useUser(id: string) {
335
- const { data, error, isLoading, mutate } = useSWR(
336
- `/api/users/${id}`,
337
- fetcher,
338
- {
339
- revalidateOnFocus: false, // Don't refetch on tab focus
340
- dedupingInterval: 5000, // Deduplicate requests within 5s
341
- staleWhileRevalidate: true, // Show stale data while fetching fresh
342
- }
343
- );
344
-
345
- return { user: data, error, isLoading, mutate };
346
- }
347
-
348
- // Multiple components calling useUser('123') = ONE network request
349
- ```
350
-
351
- ### Deduplicate Global Event Listeners
352
-
353
- ```typescript
354
- // BAD: Every component instance adds its own listener
355
- function Component() {
356
- useEffect(() => {
357
- const handler = () => { /* ... */ };
358
- window.addEventListener('resize', handler);
359
- return () => window.removeEventListener('resize', handler);
360
- }, []);
361
- }
362
-
363
- // GOOD: Single shared listener, components subscribe to derived state
364
- import { useSyncExternalStore } from 'react';
365
-
366
- let width = window.innerWidth;
367
- const listeners = new Set<() => void>();
368
-
369
- window.addEventListener('resize', () => {
370
- width = window.innerWidth;
371
- listeners.forEach(l => l());
372
- });
373
-
374
- function subscribe(listener: () => void) {
375
- listeners.add(listener);
376
- return () => listeners.delete(listener);
377
- }
378
-
379
- export function useWindowWidth() {
380
- return useSyncExternalStore(subscribe, () => width);
381
- }
382
- ```
58
+ ### Rules
59
+ - Use SWR or React Query for deduplication, caching, and revalidation (multiple components calling the same hook = one network request)
60
+ - Set `revalidateOnFocus: false` and `dedupingInterval` to control refetch behavior
61
+ - Deduplicate global event listeners with `useSyncExternalStore` instead of per-component `addEventListener`
383
62
 
384
63
  ---
385
64
 
386
65
  ## Priority 5 - MEDIUM: Re-render Optimization
387
66
 
388
- ### Defer State Reads to Usage Point
389
-
390
- ```tsx
391
- // BAD: Parent re-renders on every keystroke, all children re-render
392
- function SearchPage() {
393
- const [query, setQuery] = useState('');
394
- return (
395
- <div>
396
- <SearchInput value={query} onChange={setQuery} />
397
- <ExpensiveHeader /> {/* Re-renders on every keystroke! */}
398
- <SearchResults query={query} />
399
- </div>
400
- );
401
- }
402
-
403
- // GOOD: Isolate state to the component that needs it
404
- function SearchPage() {
405
- return (
406
- <div>
407
- <SearchSection /> {/* Contains its own state */}
408
- <ExpensiveHeader /> {/* Never re-renders from search */}
409
- </div>
410
- );
411
- }
412
-
413
- function SearchSection() {
414
- const [query, setQuery] = useState('');
415
- return (
416
- <>
417
- <SearchInput value={query} onChange={setQuery} />
418
- <SearchResults query={query} />
419
- </>
420
- );
421
- }
422
- ```
423
-
424
- ### Narrow Effect Dependencies
425
-
426
- ```typescript
427
- // BAD: Effect runs whenever any user field changes
428
- useEffect(() => {
429
- trackPageView(user.id);
430
- }, [user]); // user is an object, new reference every render
431
-
432
- // GOOD: Depend on the primitive you actually use
433
- const userId = user.id;
434
- useEffect(() => {
435
- trackPageView(userId);
436
- }, [userId]); // Only runs when the ID actually changes
437
- ```
438
-
439
- ### Subscribe to Derived State
440
-
441
- ```typescript
442
- // BAD: Re-renders on every pixel of resize
443
- function Component() {
444
- const width = useWindowWidth(); // 1024, 1023, 1022...
445
- const isMobile = width < 768;
446
- // Renders on every width change even if isMobile doesn't change
447
- }
448
-
449
- // GOOD: Only re-renders when the boolean flips
450
- function Component() {
451
- const isMobile = useSyncExternalStore(
452
- subscribe,
453
- () => window.innerWidth < 768 // returns boolean, not number
454
- );
455
- // Only re-renders when crossing the 768px threshold
456
- }
457
- ```
458
-
459
- ### Lazy State Initialization
460
-
461
- ```typescript
462
- // BAD: Expensive computation runs on every render (result is ignored after first)
463
- const [data, setData] = useState(parseExpensiveJSON(localStorage.getItem('data')));
464
-
465
- // GOOD: Function is only called once on mount
466
- const [data, setData] = useState(() => parseExpensiveJSON(localStorage.getItem('data')));
467
- ```
468
-
469
- ### Extract Memoized Components
470
-
471
- ```tsx
472
- // When a parent re-renders but a child's props don't change:
473
- const ExpensiveList = memo(function ExpensiveList({ items }: { items: Item[] }) {
474
- return items.map(item => <ExpensiveItem key={item.id} item={item} />);
475
- });
476
-
477
- // Use with useCallback for event handlers
478
- const handleClick = useCallback((id: string) => {
479
- setSelected(id);
480
- }, []); // Stable reference
481
-
482
- <ExpensiveList items={items} onClick={handleClick} />
483
- ```
484
-
485
- ### useTransition for Non-Urgent Updates
486
-
487
- ```typescript
488
- function SearchWithSuggestions() {
489
- const [query, setQuery] = useState('');
490
- const [results, setResults] = useState([]);
491
- const [isPending, startTransition] = useTransition();
492
-
493
- function handleChange(value: string) {
494
- setQuery(value); // Urgent: update input immediately
495
- startTransition(() => {
496
- setResults(filterResults(value)); // Non-urgent: can be interrupted
497
- });
498
- }
499
-
500
- return (
501
- <>
502
- <input value={query} onChange={e => handleChange(e.target.value)} />
503
- <div style={{ opacity: isPending ? 0.7 : 1 }}>
504
- <ResultsList results={results} />
505
- </div>
506
- </>
507
- );
508
- }
509
- ```
67
+ ### Rules
68
+ - Isolate state to the component that owns it; don't let a parent re-render siblings that don't use the state
69
+ - Depend on primitives in `useEffect` deps, not objects (`user.id` not `user`)
70
+ - Subscribe to derived booleans via `useSyncExternalStore` (e.g., `isMobile` not `width`)
71
+ - Use lazy state initialization: `useState(() => expensiveComputation())` not `useState(expensiveComputation())`
72
+ - Use `React.memo` + `useCallback` for expensive child components (skip if React Compiler is enabled)
73
+ - Use `useTransition` for non-urgent updates (search results filtering, large list re-renders)
510
74
 
511
75
  ---
512
76
 
513
77
  ## Priority 6 - MEDIUM: Rendering Performance
514
78
 
515
- ### content-visibility: auto for Off-Screen DOM
516
-
517
- 10x faster initial render for long pages. Browser skips layout/paint for off-screen elements.
518
-
519
- ```css
520
- .card {
521
- content-visibility: auto;
522
- contain-intrinsic-size: auto 300px; /* Estimated height to prevent scroll jump */
523
- }
524
-
525
- /* For a list of items */
526
- .list-item {
527
- content-visibility: auto;
528
- contain-intrinsic-size: auto 80px;
529
- }
530
- ```
531
-
532
- ### Hoist Static JSX Outside Components
533
-
534
- Static elements don't need to be recreated on every render.
535
-
536
- ```tsx
537
- // BAD: emptyState is recreated on every render of List
538
- function List({ items }) {
539
- const emptyState = (
540
- <div className="empty">
541
- <p>No items found</p>
542
- </div>
543
- );
544
-
545
- if (items.length === 0) return emptyState;
546
- return <ul>{items.map(renderItem)}</ul>;
547
- }
548
-
549
- // GOOD: Created once, reused forever
550
- const emptyState = (
551
- <div className="empty">
552
- <p>No items found</p>
553
- </div>
554
- );
555
-
556
- function List({ items }) {
557
- if (items.length === 0) return emptyState;
558
- return <ul>{items.map(renderItem)}</ul>;
559
- }
560
- ```
561
-
562
- ### Prevent Hydration Mismatch with Inline Script
563
-
564
- For theme, auth state, or locale that must be available before React hydrates:
565
-
566
- ```tsx
567
- // layout.tsx
568
- <head>
569
- <script
570
- dangerouslySetInnerHTML={{
571
- __html: `
572
- (function() {
573
- var theme = localStorage.getItem('theme') || 'system';
574
- if (theme === 'system') {
575
- theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
576
- }
577
- document.documentElement.setAttribute('data-theme', theme);
578
- document.documentElement.style.colorScheme = theme;
579
- })();
580
- `,
581
- }}
582
- />
583
- </head>
584
- ```
585
-
586
- This runs synchronously before React hydrates, preventing the flash of wrong theme.
587
-
588
- ### Optimize SVG Precision
589
-
590
- ```bash
591
- # Default SVG from design tools: way too many decimal places
592
- # <path d="M12.456789 34.567891 L56.789012 78.901234" />
593
-
594
- # After svgo --precision=1:
595
- # <path d="M12.5 34.6 L56.8 78.9" />
596
-
597
- npx svgo --precision=1 --multipass icons/*.svg
598
- ```
599
-
600
- Reduces SVG file size by 20-40% with no visible difference.
601
-
602
- ### Use Activity Component for Show/Hide
603
-
604
- React 19+ `<Activity>` preserves state and DOM when hiding, instead of unmounting.
605
-
606
- ```tsx
607
- import { Activity } from 'react';
608
-
609
- function TabPanel({ activeTab }) {
610
- return (
611
- <>
612
- <Activity mode={activeTab === 'editor' ? 'visible' : 'hidden'}>
613
- <CodeEditor /> {/* State preserved when hidden */}
614
- </Activity>
615
- <Activity mode={activeTab === 'preview' ? 'visible' : 'hidden'}>
616
- <Preview /> {/* State preserved when hidden */}
617
- </Activity>
618
- </>
619
- );
620
- }
621
- ```
622
-
623
- ### Explicit Conditional Rendering
624
-
625
- ```tsx
626
- // BAD: && can render "0" or "NaN" as text
627
- {count && <Badge count={count} />}
628
- // If count is 0, renders "0" as text in the DOM
629
-
630
- // GOOD: Explicit boolean check
631
- {count > 0 ? <Badge count={count} /> : null}
632
-
633
- // GOOD: Double negation for truthy check
634
- {!!items.length ? <List items={items} /> : <EmptyState />}
635
- ```
79
+ ### Rules
80
+ - Use `content-visibility: auto` with `contain-intrinsic-size` for off-screen DOM (10x faster initial render for long pages)
81
+ - Hoist static JSX outside component functions (created once, reused forever)
82
+ - Use an inline `<script>` in `<head>` for theme/auth/locale to prevent hydration mismatch
83
+ - Optimize SVG precision: `npx svgo --precision=1 --multipass` reduces file size 20-40%
84
+ - Use React 19+ `<Activity>` to preserve state/DOM when hiding tab panels instead of unmounting
85
+ - Use explicit boolean checks for conditional rendering (`count > 0 ?` not `count &&` which can render "0")
636
86
 
637
87
  ---
638
88
 
639
89
  ## Priority 7 - LOW-MEDIUM: JavaScript Performance
640
90
 
641
- ### Build Index Maps for O(1) Lookups
642
-
643
- ```typescript
644
- // BAD: O(n) lookup on every render or event
645
- function findUser(users: User[], id: string) {
646
- return users.find(u => u.id === id); // Scans entire array
647
- }
648
-
649
- // GOOD: O(1) lookup after one-time O(n) index build
650
- const userIndex = useMemo(() => {
651
- const map = new Map<string, User>();
652
- for (const user of users) {
653
- map.set(user.id, user);
654
- }
655
- return map;
656
- }, [users]);
657
-
658
- function findUser(id: string) {
659
- return userIndex.get(id); // Instant lookup
660
- }
661
- ```
662
-
663
- ### Set/Map for Membership Checks
664
-
665
- ```typescript
666
- // BAD: O(n) per check
667
- const selectedIds = ['a', 'b', 'c', ...hundredsMore];
668
- items.filter(item => selectedIds.includes(item.id)); // O(n*m)
669
-
670
- // GOOD: O(1) per check
671
- const selectedSet = new Set(selectedIds);
672
- items.filter(item => selectedSet.has(item.id)); // O(n)
673
- ```
674
-
675
- ### Combine Array Iterations
676
-
677
- ```typescript
678
- // BAD: Three passes over the array
679
- const active = users.filter(u => u.active);
680
- const names = active.map(u => u.name);
681
- const sorted = names.sort();
682
-
683
- // GOOD: Single pass + sort
684
- const names: string[] = [];
685
- for (const u of users) {
686
- if (u.active) names.push(u.name);
687
- }
688
- names.sort();
689
- ```
690
-
691
- This matters when arrays have 1000+ items or the operation runs frequently (on scroll, on keystroke).
692
-
693
- ### Cache Storage API Calls
694
-
695
- ```typescript
696
- // BAD: Synchronous localStorage call on every render
697
- function useTheme() {
698
- const [theme, setTheme] = useState(localStorage.getItem('theme') || 'light');
699
- // localStorage.getItem is synchronous and blocks the main thread
700
- }
701
-
702
- // GOOD: Read once, cache in memory
703
- let cachedTheme: string | null = null;
704
- function getTheme() {
705
- if (cachedTheme === null) {
706
- cachedTheme = localStorage.getItem('theme') || 'light';
707
- }
708
- return cachedTheme;
709
- }
710
-
711
- function setTheme(theme: string) {
712
- cachedTheme = theme;
713
- localStorage.setItem('theme', theme); // Write-through
714
- }
715
- ```
716
-
717
- ### toSorted() Instead of sort()
718
-
719
- ```typescript
720
- // BAD: Mutates the original array (breaks React state rules)
721
- const sorted = items.sort((a, b) => a.name.localeCompare(b.name));
722
- // items is now mutated! React won't detect the change
723
-
724
- // GOOD: Returns new sorted array (immutable, React-safe)
725
- const sorted = items.toSorted((a, b) => a.name.localeCompare(b.name));
726
- // items is unchanged, sorted is a new array
727
-
728
- // Also: toReversed(), toSpliced(), with()
729
- const reversed = items.toReversed();
730
- const withRemoval = items.toSpliced(2, 1); // Remove item at index 2
731
- const withReplacement = items.with(3, newItem); // Replace item at index 3
732
- ```
91
+ ### Rules
92
+ - Build `Map` indexes for O(1) lookups instead of `.find()` on arrays
93
+ - Use `Set` for membership checks instead of `.includes()` on arrays
94
+ - Combine `.filter().map().sort()` into a single loop when arrays have 1000+ items
95
+ - Cache `localStorage` reads in memory; read once, write-through on set
96
+ - Use `toSorted()`, `toReversed()`, `toSpliced()`, `with()` for immutable array operations (React-safe, no mutation)
733
97
 
734
98
  ---
735
99