@ericrisco/rsc 3.3.1 → 3.4.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.
Files changed (54) hide show
  1. package/README.md +2 -2
  2. package/manifest.json +43 -22
  3. package/package.json +1 -1
  4. package/scripts/lib/default-skill-floor.js +5 -1
  5. package/scripts/lib/domains.js +2 -2
  6. package/scripts/lib/retired-skills.js +4 -0
  7. package/skills/design/SKILL.md +4 -3
  8. package/skills/design/evals/cases.yaml +2 -2
  9. package/skills/react/SKILL.md +2 -1
  10. package/skills/ux/SKILL.md +149 -0
  11. package/skills/ux/THIRD_PARTY.md +32 -0
  12. package/skills/ux/evals/cases.yaml +40 -0
  13. package/skills/ux/references/flows.md +124 -0
  14. package/skills/ux/upstream/UPSTREAM.json +59 -0
  15. package/skills/ux/upstream/ux-designer/GUIDE.md +180 -0
  16. package/skills/ux/upstream/ux-designer/LICENSE +21 -0
  17. package/skills/ux/upstream/ux-designer/references/01-core-principles.md +251 -0
  18. package/skills/ux/upstream/ux-designer/references/02-laws-of-ux.md +330 -0
  19. package/skills/ux/upstream/ux-designer/references/03-accessibility.md +411 -0
  20. package/skills/ux/upstream/ux-designer/references/04-visual-design.md +433 -0
  21. package/skills/ux/upstream/ux-designer/references/05-information-architecture.md +432 -0
  22. package/skills/ux/upstream/ux-designer/references/06-interaction-design.md +495 -0
  23. package/skills/ux/upstream/ux-designer/references/07-forms-and-inputs.md +491 -0
  24. package/skills/ux/upstream/ux-designer/references/08-mobile-ux.md +539 -0
  25. package/skills/ux/upstream/ux-designer/references/09-ux-writing.md +493 -0
  26. package/skills/ux/upstream/ux-designer/references/10-user-research.md +533 -0
  27. package/skills/ux/upstream/ux-designer/references/11-design-systems.md +618 -0
  28. package/skills/ux/upstream/ux-designer/references/12a-presence-awareness.md +395 -0
  29. package/skills/ux/upstream/ux-designer/references/12b-conflict-resolution-sync.md +420 -0
  30. package/skills/ux/upstream/ux-designer/references/13a-canvas-navigation.md +388 -0
  31. package/skills/ux/upstream/ux-designer/references/13b-canvas-objects-performance.md +559 -0
  32. package/skills/ux/upstream/ux-designer/references/14-ai-ux-patterns.md +511 -0
  33. package/skills/ux/upstream/ux-designer/references/15-ethical-design.md +419 -0
  34. package/skills/ux/upstream/ux-designer/references/16-onboarding.md +430 -0
  35. package/skills/ux/upstream/ux-designer/references/17-notifications.md +461 -0
  36. package/skills/ux/upstream/ux-designer/references/18-data-visualization.md +453 -0
  37. package/skills/ux/upstream/ux-designer/references/19-search-ux.md +479 -0
  38. package/skills/ux/upstream/ux-designer/references/20-emotional-design.md +382 -0
  39. package/skills/ux/upstream/ux-designer/references/21-data-tables.md +629 -0
  40. package/skills/ux/upstream/ux-designer/references/22-performance-ux.md +626 -0
  41. package/skills/ux/upstream/ux-designer/references/23-internationalization.md +347 -0
  42. package/skills/ux/upstream/ux-designer/references/24-voice-and-multimodal.md +308 -0
  43. package/skills/ux/upstream/ux-designer/references/25a-barcode-camera-scanning.md +342 -0
  44. package/skills/ux/upstream/ux-designer/references/25b-barcode-scanner-workflows.md +412 -0
  45. package/skills/ux/upstream/ux-designer/references/25c-barcode-display.md +277 -0
  46. package/skills/ux/upstream/ux-usability-foundations/GUIDE.md +285 -0
  47. package/skills/ux/upstream/ux-usability-foundations/LICENSE +21 -0
  48. package/skills/ux/upstream/ux-usability-foundations/references/anti-patterns.md +267 -0
  49. package/skills/ux/upstream/ux-usability-foundations/references/checklists.md +130 -0
  50. package/skills/ux/upstream/ux-usability-foundations/references/decision-prompts.md +340 -0
  51. package/skills/ux/upstream/ux-usability-foundations/references/principle-cards.md +449 -0
  52. package/targets/clone-bootstrap.mjs +1 -1
  53. package/skills/ui-engineering/SKILL.md +0 -113
  54. package/skills/ui-engineering/evals/cases.yaml +0 -38
@@ -0,0 +1,626 @@
1
+ # Performance & Loading UX
2
+
3
+ Perceived performance is often more important than actual performance. Users judge an app's speed not by stopwatch measurements but by how fast it *feels*. This reference covers techniques for making interfaces feel instant, responsive, and reliable.
4
+
5
+ ---
6
+
7
+ ## Contents
8
+
9
+ - Perceived Performance
10
+ - Loading Indicator Decision Tree
11
+ - Skeleton Screens
12
+ - Optimistic Updates
13
+ - Progress Indicators
14
+ - Image Loading Strategies
15
+ - Content Layout Shift Prevention
16
+ - Offline-First & Cached State UX
17
+ - Prefetching & Preloading
18
+ - Key Metrics
19
+ - Anti-Patterns
20
+ - Sources
21
+
22
+ ---
23
+
24
+ ## Perceived Performance
25
+
26
+ ### Response Time Thresholds
27
+
28
+ | Duration | Perception | UX Treatment |
29
+ |----------|-----------|--------------|
30
+ | < 100ms | Instant | No indicator needed |
31
+ | 100-300ms | Slight delay | Subtle state change (button press) |
32
+ | 300ms-1s | Noticeable | Show spinner or progress hint |
33
+ | 1-5s | Waiting | Skeleton screen or progress bar |
34
+ | 5-10s | Frustrated | Progress bar with estimate + cancel |
35
+ | > 10s | Abandonment risk | Progress steps, background processing option |
36
+
37
+ ### Techniques for Feeling Fast
38
+
39
+ 1. **Optimistic updates** — Apply changes immediately, sync later
40
+ 2. **Skeleton screens** — Show content shape before data arrives
41
+ 3. **Progressive loading** — Show most important content first
42
+ 4. **Prefetching** — Load likely-next content before user requests it
43
+ 5. **Instant navigation** — Use client-side routing with prefetch
44
+ 6. **Animation as distraction** — Meaningful transitions mask load time
45
+
46
+ ---
47
+
48
+ ## Loading Indicator Decision Tree
49
+
50
+ ```
51
+ How long will the operation take?
52
+ ├── < 100ms (instant)?
53
+ │ └── → No indicator (just update the UI)
54
+ ├── 100ms - 1s (brief)?
55
+ │ ├── User triggered the action?
56
+ │ │ └── → Button loading state (spinner replaces label or inline spinner)
57
+ │ └── Background/automatic?
58
+ │ └── → No indicator (avoid flicker)
59
+ ├── 1s - 5s (noticeable)?
60
+ │ ├── Loading content for a region/page?
61
+ │ │ └── → Skeleton screen (matches target layout)
62
+ │ ├── Processing user action?
63
+ │ │ └── → Inline spinner with status text ("Saving...")
64
+ │ └── Unknown duration?
65
+ │ └── → Indeterminate progress bar
66
+ ├── 5s - 30s (long)?
67
+ │ ├── Duration is predictable?
68
+ │ │ └── → Determinate progress bar with percentage
69
+ │ └── Duration is unpredictable?
70
+ │ └── → Indeterminate progress with stage labels
71
+ └── > 30s (very long)?
72
+ └── → Background task with notification on completion
73
+ ("We'll email you when your export is ready")
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Skeleton Screens
79
+
80
+ Skeleton screens show a low-fidelity preview of the content layout before data loads. They reduce perceived wait time and prevent layout shifts.
81
+
82
+ ### Basic Skeleton
83
+
84
+ ```css
85
+ .skeleton {
86
+ background: #e5e7eb;
87
+ border-radius: 4px;
88
+ position: relative;
89
+ overflow: hidden;
90
+ }
91
+
92
+ /* Shimmer animation */
93
+ .skeleton::after {
94
+ content: '';
95
+ position: absolute;
96
+ inset: 0;
97
+ background: linear-gradient(
98
+ 90deg,
99
+ transparent 0%,
100
+ rgba(255, 255, 255, 0.4) 50%,
101
+ transparent 100%
102
+ );
103
+ animation: skeleton-shimmer 1.5s infinite;
104
+ }
105
+
106
+ @keyframes skeleton-shimmer {
107
+ 0% { transform: translateX(-100%); }
108
+ 100% { transform: translateX(100%); }
109
+ }
110
+ ```
111
+
112
+ ### Skeleton Variants
113
+
114
+ ```html
115
+ <!-- Text skeleton -->
116
+ <div class="skeleton skeleton--text" style="width: 80%; height: 16px;"></div>
117
+ <div class="skeleton skeleton--text" style="width: 60%; height: 16px;"></div>
118
+
119
+ <!-- Avatar skeleton -->
120
+ <div class="skeleton skeleton--circle" style="width: 40px; height: 40px;"></div>
121
+
122
+ <!-- Image skeleton -->
123
+ <div class="skeleton skeleton--image" style="width: 100%; aspect-ratio: 16/9;"></div>
124
+
125
+ <!-- Card skeleton -->
126
+ <div class="skeleton-card">
127
+ <div class="skeleton skeleton--image" style="height: 200px;"></div>
128
+ <div style="padding: 16px;">
129
+ <div class="skeleton skeleton--text" style="width: 70%; height: 20px;"></div>
130
+ <div class="skeleton skeleton--text" style="width: 100%; height: 14px; margin-top: 8px;"></div>
131
+ <div class="skeleton skeleton--text" style="width: 40%; height: 14px; margin-top: 8px;"></div>
132
+ </div>
133
+ </div>
134
+ ```
135
+
136
+ ### Skeleton Best Practices
137
+
138
+ | Do | Don't |
139
+ |----|-------|
140
+ | Match the actual content layout | Use generic shapes unrelated to content |
141
+ | Use subtle animation (shimmer) | Use pulsing that draws too much attention |
142
+ | Show for > 300ms loads | Show for instant loads (causes flicker) |
143
+ | Transition smoothly to real content | Pop content in abruptly |
144
+ | Keep skeleton visible while data loads | Replace skeleton with spinner |
145
+
146
+ ### Skeleton Transition
147
+
148
+ ```javascript
149
+ function showContent(container, data) {
150
+ const skeleton = container.querySelector('.skeleton-wrapper');
151
+ const content = container.querySelector('.content-wrapper');
152
+
153
+ // Populate content using safe DOM methods (e.g., textContent,
154
+ // createElement, or a framework's reactive rendering)
155
+ renderDataToDOM(content, data);
156
+
157
+ // Cross-fade transition
158
+ skeleton.style.opacity = '0';
159
+ content.style.opacity = '1';
160
+
161
+ setTimeout(() => skeleton.remove(), 300);
162
+ }
163
+ ```
164
+
165
+ ```css
166
+ .skeleton-wrapper,
167
+ .content-wrapper {
168
+ transition: opacity 300ms ease;
169
+ }
170
+
171
+ .content-wrapper {
172
+ opacity: 0;
173
+ }
174
+ ```
175
+
176
+ ---
177
+
178
+ ## Optimistic Updates
179
+
180
+ Apply changes to the UI immediately before server confirmation, then reconcile.
181
+
182
+ ### Pattern
183
+
184
+ ```javascript
185
+ async function toggleFavorite(itemId) {
186
+ // 1. Optimistic: update UI immediately
187
+ const previousState = getItem(itemId).isFavorite;
188
+ updateItemUI(itemId, { isFavorite: !previousState });
189
+
190
+ try {
191
+ // 2. Send to server
192
+ await api.toggleFavorite(itemId);
193
+ // Success: UI already correct, nothing to do
194
+ } catch (error) {
195
+ // 3. Rollback on failure
196
+ updateItemUI(itemId, { isFavorite: previousState });
197
+ showToast('Failed to update. Please try again.', { type: 'error' });
198
+ }
199
+ }
200
+ ```
201
+
202
+ ### When to Use Optimistic Updates
203
+
204
+ | Use Optimistic | Don't Use |
205
+ |---------------|-----------|
206
+ | Toggles (like, favorite, bookmark) | Financial transactions |
207
+ | Status changes (read/unread) | Destructive actions (delete) |
208
+ | Form field auto-save | Multi-step workflows |
209
+ | Reordering lists | Actions requiring server validation |
210
+ | Adding/removing tags | External system integrations |
211
+
212
+ ### Optimistic Update UX Requirements
213
+
214
+ - Always have a rollback path
215
+ - Show subtle "saving" indicator (not blocking)
216
+ - On failure: revert UI + show clear error message
217
+ - On conflict: show resolution UI (merge dialog)
218
+ - Debounce rapid changes (typing, slider adjustments)
219
+
220
+ ---
221
+
222
+ ## Progress Indicators
223
+
224
+ ### Indeterminate Progress
225
+
226
+ For operations where duration is unknown.
227
+
228
+ ```css
229
+ /* Indeterminate progress bar */
230
+ .progress-bar--indeterminate {
231
+ height: 4px;
232
+ background: #e5e7eb;
233
+ border-radius: 2px;
234
+ overflow: hidden;
235
+ }
236
+
237
+ .progress-bar--indeterminate::after {
238
+ content: '';
239
+ display: block;
240
+ height: 100%;
241
+ width: 40%;
242
+ background: #3b82f6;
243
+ border-radius: 2px;
244
+ animation: indeterminate 1.5s infinite ease-in-out;
245
+ }
246
+
247
+ @keyframes indeterminate {
248
+ 0% { transform: translateX(-100%); }
249
+ 100% { transform: translateX(350%); }
250
+ }
251
+ ```
252
+
253
+ ### Determinate Progress
254
+
255
+ For operations where progress can be measured.
256
+
257
+ ```html
258
+ <div class="progress-container">
259
+ <div class="progress-bar" role="progressbar"
260
+ aria-valuenow="65" aria-valuemin="0" aria-valuemax="100">
261
+ <div class="progress-bar__fill" style="width: 65%"></div>
262
+ </div>
263
+ <span class="progress-label">65% — Uploading 3 of 5 files</span>
264
+ </div>
265
+ ```
266
+
267
+ ### Multi-Step Progress
268
+
269
+ ```
270
+ ┌──────────────────────────────────────────────────────────────┐
271
+ │ Importing data... │
272
+ │ │
273
+ │ ✓ Validating file format │
274
+ │ ✓ Parsing 1,234 records │
275
+ │ ⟳ Importing to database (847 of 1,234) │
276
+ │ ○ Running post-import checks │
277
+ │ │
278
+ │ ████████████████████░░░░░░░░░░ 68% [Cancel import] │
279
+ └──────────────────────────────────────────────────────────────┘
280
+ ```
281
+
282
+ ### Button Loading States
283
+
284
+ ```css
285
+ .btn--loading {
286
+ position: relative;
287
+ color: transparent; /* Hide text */
288
+ pointer-events: none;
289
+ }
290
+
291
+ .btn--loading::after {
292
+ content: '';
293
+ position: absolute;
294
+ width: 16px;
295
+ height: 16px;
296
+ top: 50%;
297
+ left: 50%;
298
+ margin: -8px 0 0 -8px;
299
+ border: 2px solid rgba(255, 255, 255, 0.3);
300
+ border-top-color: white;
301
+ border-radius: 50%;
302
+ animation: button-spin 0.6s linear infinite;
303
+ }
304
+
305
+ @keyframes button-spin {
306
+ to { transform: rotate(360deg); }
307
+ }
308
+ ```
309
+
310
+ **Button loading best practices:**
311
+ - Disable the button to prevent double-submit
312
+ - Keep button dimensions constant (no layout shift)
313
+ - Show spinner inside the button, not replacing it
314
+ - Optionally change label: "Save" → "Saving..."
315
+
316
+ ---
317
+
318
+ ## Image Loading Strategies
319
+
320
+ ### Lazy Loading
321
+
322
+ Only load images when they enter (or approach) the viewport.
323
+
324
+ ```html
325
+ <!-- Native lazy loading -->
326
+ <img src="photo.jpg" loading="lazy" alt="Description"
327
+ width="800" height="600">
328
+ ```
329
+
330
+ ### Blur-Up / LQIP (Low Quality Image Placeholder)
331
+
332
+ Show a tiny, blurred preview that transitions to the full image.
333
+
334
+ ```html
335
+ <div class="image-wrapper">
336
+ <img class="image-placeholder" src="photo-20px.jpg"
337
+ style="filter: blur(20px); transform: scale(1.1);"
338
+ aria-hidden="true">
339
+ <img class="image-full" src="photo-800px.jpg" loading="lazy"
340
+ alt="Description"
341
+ onload="this.classList.add('loaded')">
342
+ </div>
343
+ ```
344
+
345
+ ```css
346
+ .image-wrapper {
347
+ position: relative;
348
+ overflow: hidden;
349
+ }
350
+
351
+ .image-placeholder {
352
+ position: absolute;
353
+ inset: 0;
354
+ width: 100%;
355
+ height: 100%;
356
+ object-fit: cover;
357
+ }
358
+
359
+ .image-full {
360
+ opacity: 0;
361
+ transition: opacity 300ms ease;
362
+ }
363
+
364
+ .image-full.loaded {
365
+ opacity: 1;
366
+ }
367
+ ```
368
+
369
+ ### Responsive Images
370
+
371
+ ```html
372
+ <img
373
+ srcset="photo-400w.jpg 400w,
374
+ photo-800w.jpg 800w,
375
+ photo-1200w.jpg 1200w"
376
+ sizes="(max-width: 600px) 400px,
377
+ (max-width: 1000px) 800px,
378
+ 1200px"
379
+ src="photo-800w.jpg"
380
+ alt="Description"
381
+ loading="lazy"
382
+ >
383
+ ```
384
+
385
+ ### Image Loading Decision Tree
386
+
387
+ ```
388
+ What type of image is it?
389
+ ├── Above the fold (hero, logo, critical UI)?
390
+ │ └── → Eager load, preload via <link>, use srcset
391
+ ├── Below the fold (content images)?
392
+ │ └── → loading="lazy", use LQIP/blur-up for large images
393
+ ├── Thumbnail in a list/grid?
394
+ │ └── → loading="lazy", fixed dimensions, skeleton placeholder
395
+ ├── Background/decorative?
396
+ │ └── → CSS background with lazy class, low priority
397
+ └── User avatar?
398
+ └── → Eager load if visible, fixed dimensions, fallback initial
399
+ ```
400
+
401
+ ---
402
+
403
+ ## Content Layout Shift Prevention
404
+
405
+ Layout shifts are disorienting and frustrating. They happen when content dimensions change after initial render.
406
+
407
+ ### Core Web Vitals: CLS
408
+
409
+ - **Good:** CLS < 0.1
410
+ - **Needs improvement:** 0.1 - 0.25
411
+ - **Poor:** CLS > 0.25
412
+
413
+ ### Prevention Techniques
414
+
415
+ #### Reserve Space for Images
416
+
417
+ ```css
418
+ /* Always specify dimensions */
419
+ img {
420
+ width: 100%;
421
+ height: auto;
422
+ aspect-ratio: 16 / 9; /* Reserve space before load */
423
+ }
424
+
425
+ /* Container with aspect ratio */
426
+ .image-container {
427
+ position: relative;
428
+ width: 100%;
429
+ aspect-ratio: 4 / 3;
430
+ background: #f3f4f6;
431
+ }
432
+ ```
433
+
434
+ #### Reserve Space for Fonts
435
+
436
+ ```css
437
+ /* Use font-display to control FOIT/FOUT */
438
+ @font-face {
439
+ font-family: 'CustomFont';
440
+ src: url('font.woff2') format('woff2');
441
+ font-display: swap; /* Show fallback immediately, swap when loaded */
442
+ }
443
+
444
+ /* Match fallback font metrics to reduce shift */
445
+ body {
446
+ font-family: 'CustomFont', system-ui, sans-serif;
447
+ font-size-adjust: 0.5; /* Normalize x-height */
448
+ }
449
+ ```
450
+
451
+ #### Reserve Space for Dynamic Content
452
+
453
+ ```css
454
+ /* Fixed-height containers for dynamic content */
455
+ .ad-slot {
456
+ min-height: 250px;
457
+ background: #f3f4f6;
458
+ }
459
+
460
+ .notification-area {
461
+ min-height: 0;
462
+ transition: min-height 300ms ease; /* Smooth expansion */
463
+ }
464
+ ```
465
+
466
+ #### Avoid CLS-Causing Patterns
467
+
468
+ | Pattern | Problem | Solution |
469
+ |---------|---------|----------|
470
+ | Images without dimensions | Jump when loaded | Always set width/height or aspect-ratio |
471
+ | Injected content above viewport | Pushes content down | Insert below or use overlay |
472
+ | Web fonts without fallback | Text reflows | Use font-display: swap + size-adjust |
473
+ | Dynamic ads/banners | Layout shift on load | Reserve fixed space |
474
+ | Late-loading components | Layout reflowing | Use skeleton placeholders |
475
+
476
+ ---
477
+
478
+ ## Offline-First & Cached State UX
479
+
480
+ ### Connection Status
481
+
482
+ ```css
483
+ /* Offline indicator */
484
+ .offline-banner {
485
+ position: fixed;
486
+ bottom: 0;
487
+ left: 0;
488
+ right: 0;
489
+ padding: 8px 16px;
490
+ background: #fef3c7;
491
+ color: #92400e;
492
+ text-align: center;
493
+ font-size: 14px;
494
+ z-index: 9999;
495
+ transform: translateY(100%);
496
+ transition: transform 300ms ease;
497
+ }
498
+
499
+ .offline-banner--visible {
500
+ transform: translateY(0);
501
+ }
502
+ ```
503
+
504
+ ### Stale Content Indicators
505
+
506
+ ```html
507
+ <div class="stale-notice">
508
+ <span>Last updated 5 minutes ago</span>
509
+ <button class="refresh-btn">Refresh</button>
510
+ </div>
511
+ ```
512
+
513
+ **Stale data guidelines:**
514
+ - Show "last updated" timestamp for cached data
515
+ - Provide manual refresh option
516
+ - Auto-refresh when connection restores
517
+ - Differentiate between "cached" and "live" data visually
518
+ - Never silently show outdated data as if it were current
519
+
520
+ ### Offline-Capable Actions
521
+
522
+ | Action Type | Offline Behavior |
523
+ |-------------|-----------------|
524
+ | Read cached data | Show with "offline" indicator |
525
+ | Create new items | Queue locally, sync when online |
526
+ | Edit existing items | Queue changes, show "pending sync" |
527
+ | Delete items | Mark as "pending delete", hide from view |
528
+ | Search | Search local cache only, note limitation |
529
+
530
+ ### Sync Status Indicators
531
+
532
+ ```
533
+ ┌──────────────────────────────────────────┐
534
+ │ ✓ All changes saved │ — Everything synced
535
+ │ ⟳ Saving... │ — Sync in progress
536
+ │ ⚠ Offline — changes will sync later │ — Queued changes
537
+ │ ✕ Save failed — click to retry │ — Error state
538
+ └──────────────────────────────────────────┘
539
+ ```
540
+
541
+ ---
542
+
543
+ ## Prefetching & Preloading
544
+
545
+ ### Link Prefetching
546
+
547
+ ```html
548
+ <!-- Prefetch likely next pages -->
549
+ <link rel="prefetch" href="/dashboard">
550
+ <link rel="prefetch" href="/api/user-data.json">
551
+
552
+ <!-- Preload critical resources -->
553
+ <link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
554
+ <link rel="preload" href="/hero.jpg" as="image">
555
+ ```
556
+
557
+ ### Hover Prefetch
558
+
559
+ ```javascript
560
+ // Prefetch on hover (before click)
561
+ document.querySelectorAll('a[data-prefetch]').forEach(link => {
562
+ let prefetched = false;
563
+
564
+ link.addEventListener('mouseenter', () => {
565
+ if (prefetched) return;
566
+ prefetched = true;
567
+
568
+ const prefetchLink = document.createElement('link');
569
+ prefetchLink.rel = 'prefetch';
570
+ prefetchLink.href = link.href;
571
+ document.head.appendChild(prefetchLink);
572
+ });
573
+ });
574
+ ```
575
+
576
+ ### Prefetch Strategy Decision
577
+
578
+ | Signal | Action |
579
+ |--------|--------|
580
+ | User hovers over link | Prefetch page |
581
+ | User types in search | Prefetch top results |
582
+ | User reaches 80% of list | Prefetch next page |
583
+ | User is on step 2 of 4 | Prefetch step 3 resources |
584
+ | Viewport approaches lazy content | Prefetch images |
585
+
586
+ ---
587
+
588
+ ## Key Metrics
589
+
590
+ | Metric | Value | Context |
591
+ |--------|-------|---------|
592
+ | Instant response | < 100ms | No loading indicator needed |
593
+ | Skeleton threshold | > 300ms | Show skeleton after this delay |
594
+ | Toast error display | >= 5s | Longer than success toasts |
595
+ | CLS target | < 0.1 | Core Web Vitals "good" |
596
+ | LCP target | < 2.5s | Largest Contentful Paint |
597
+ | FID target | < 100ms | First Input Delay |
598
+ | INP target | < 200ms | Interaction to Next Paint |
599
+ | Optimistic rollback | < 5s | Show error if no confirmation |
600
+ | Prefetch hover delay | 65ms | Wait before prefetching |
601
+
602
+ ---
603
+
604
+ ## Anti-Patterns
605
+
606
+ 1. **Spinner for everything** - Use skeletons for content, spinners for actions
607
+ 2. **Full-page loader** - Block only the region that's loading, not the whole page
608
+ 3. **No minimum display time** - Skeleton that flashes for 50ms is worse than nothing
609
+ 4. **Layout shifts on load** - Always reserve space for async content
610
+ 5. **Silent failures** - User doesn't know their action failed
611
+ 6. **Optimistic without rollback** - Showing success then silently losing data
612
+ 7. **Blocking UI during save** - Use background save with status indicator
613
+ 8. **No offline indication** - Users assume they're connected
614
+ 9. **Stale data shown as current** - Always indicate data freshness
615
+ 10. **Retry without feedback** - Show what's happening during retry attempts
616
+
617
+ ---
618
+
619
+ ## Sources
620
+
621
+ - [web.dev - Core Web Vitals](https://web.dev/vitals/) - Google's performance metrics
622
+ - [web.dev - Optimize CLS](https://web.dev/optimize-cls/) - Layout shift prevention
623
+ - [Nielsen Norman Group - Response Times](https://www.nngroup.com/articles/response-times-3-important-limits/) - Perception thresholds
624
+ - [Smashing Magazine - Skeleton Screens](https://www.smashingmagazine.com/2020/04/skeleton-screens-react/) - Implementation patterns
625
+ - [Luke Wroblewski - Mobile Input](https://www.lukew.com/ff/entry.asp?1950) - Perceived performance research
626
+ - [Instant.page](https://instant.page/) - Prefetch on hover library