cinematic-scroll-skill 2.1.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 (61) hide show
  1. package/COMPATIBILITY.md +244 -0
  2. package/LICENSE +21 -0
  3. package/MODELS.md +92 -0
  4. package/README.md +250 -0
  5. package/SKILL.md +1003 -0
  6. package/audit-mode.md +497 -0
  7. package/bin/install.mjs +91 -0
  8. package/compile-choreography.mjs +296 -0
  9. package/decision-log.md +241 -0
  10. package/examples/GETTING_STARTED.md +279 -0
  11. package/examples/KNOWN_ISSUES.md +50 -0
  12. package/examples/PROMPTS.md +166 -0
  13. package/examples/luxe/README.md +88 -0
  14. package/examples/luxe/index.html +662 -0
  15. package/examples/noir/README.md +72 -0
  16. package/examples/noir/index.html +634 -0
  17. package/examples/pop/README.md +81 -0
  18. package/examples/pop/index.html +711 -0
  19. package/examples/renaissance/README.md +39 -0
  20. package/examples/renaissance/index.html +648 -0
  21. package/examples/studio/README.md +77 -0
  22. package/examples/studio/chapters.js +105 -0
  23. package/examples/studio/index.html +520 -0
  24. package/manifest.json +92 -0
  25. package/manifest.md +136 -0
  26. package/package.json +56 -0
  27. package/references/film-archetypes.md +211 -0
  28. package/references/performance-budget.md +499 -0
  29. package/references/scroll-patterns.md +693 -0
  30. package/scroll-choreography-compilation.md +543 -0
  31. package/scroll-choreography.json +1512 -0
  32. package/taste-guardrails.md +164 -0
  33. package/templates/nextjs/.env.example +41 -0
  34. package/templates/nextjs/app/api/fal/proxy/route.ts +33 -0
  35. package/templates/nextjs/app/api/fal/webhook/route.ts +132 -0
  36. package/templates/nextjs/app/api/generate-edition-asset/route.ts +66 -0
  37. package/templates/nextjs/app/globals.css +80 -0
  38. package/templates/nextjs/app/layout.tsx +21 -0
  39. package/templates/nextjs/app/page.tsx +10 -0
  40. package/templates/nextjs/components/ChapterDemoVisual.tsx +212 -0
  41. package/templates/nextjs/components/ChapterScene.tsx +373 -0
  42. package/templates/nextjs/components/EditionsPage.tsx +116 -0
  43. package/templates/nextjs/components/SmoothScrollProvider.tsx +8 -0
  44. package/templates/nextjs/lib/api-guard.ts +110 -0
  45. package/templates/nextjs/lib/editions-manifest.ts +224 -0
  46. package/templates/nextjs/lib/fal-client.ts +12 -0
  47. package/templates/nextjs/lib/fal-generate.ts +86 -0
  48. package/templates/nextjs/lib/fal-models.ts +213 -0
  49. package/templates/nextjs/lib/prompt-contract.ts +97 -0
  50. package/templates/nextjs/lib/use-device.ts +42 -0
  51. package/templates/nextjs/lib/use-lenis.ts +35 -0
  52. package/templates/nextjs/next.config.ts +29 -0
  53. package/templates/nextjs/package-lock.json +6455 -0
  54. package/templates/nextjs/package.json +41 -0
  55. package/templates/nextjs/package.patch.json +28 -0
  56. package/templates/nextjs/postcss.config.js +6 -0
  57. package/templates/nextjs/scripts/generate-chapter-assets.mjs +243 -0
  58. package/templates/nextjs/scripts/setup.mjs +170 -0
  59. package/templates/nextjs/tailwind.config.ts +37 -0
  60. package/templates/nextjs/tsconfig.json +23 -0
  61. package/troubleshooting.md +1284 -0
@@ -0,0 +1,499 @@
1
+ # Performance Budget
2
+
3
+ > Target 60fps. Only animate `transform` and `opacity`. Use `will-change` strategically, never globally.
4
+ > Deviation from any constraint in this document requires written justification and explicit sign-off.
5
+
6
+ ---
7
+
8
+ ## 1. The 60fps Contract
9
+
10
+ ### Frame Budget
11
+
12
+ | Metric | Constraint | Rationale |
13
+ |--------|-----------|-----------|
14
+ | Frame duration | 16.67ms max | 60fps = 1000ms / 60 frames |
15
+ | JavaScript execution | 10ms max per frame | Leaves 6.67ms for browser compositing + overhead |
16
+ | Scroll handler execution | 2ms max | Excess causes input lag |
17
+ | RAF callback duration | 1.5ms max | Velocity tracking, progress updates |
18
+ | Layout read batch window | 100ms | Batch all reads, never interleave read/write |
19
+
20
+ ### Permitted Properties
21
+
22
+ These are the **ONLY** properties that may be animated during scroll-driven interactions:
23
+
24
+ ```
25
+ translate3d(x, y, z) — preferred over translateX/Y (forces compositing)
26
+ scale(x, y) — independent axis scaling allowed
27
+ rotateX/Y/Z(deg) — 3D rotation requires perspective on parent
28
+ opacity — 0 to 1 only (no partial opacity for text)
29
+ ```
30
+
31
+ ### Forbidden Properties
32
+
33
+ Never animate these during scroll. No exceptions.
34
+
35
+ ```
36
+ width, height — triggers layout recalculation
37
+ top, left, right, bottom — triggers layout
38
+ margin, padding — triggers layout on parent + siblings
39
+ font-size, line-height — triggers text reflow
40
+ border-radius — triggers layout recalculation
41
+ box-shadow — causes repaint on every frame
42
+ filter: blur() — GPU killer; dramatically more expensive than opacity
43
+ filter: brightness() — forces main-thread compositing
44
+ filter: contrast() — forces main-thread compositing
45
+ clip-path (animated) — main-thread cost; can blow the frame budget
46
+ ```
47
+
48
+ ### Scroll Handler Rules
49
+
50
+ 1. **No layout reads in scroll handlers.** `getBoundingClientRect`, `offsetHeight`, `clientWidth`, and `scrollHeight` are forbidden inside `onScroll`, ScrollTrigger callbacks, and IntersectionObserver handlers. Cache values on resize. Use `ResizeObserver` for element dimension changes.
51
+
52
+ 2. **No forced synchronous layout.** Do not write a style then read a layout property in the same frame. Batch all writes after all reads.
53
+
54
+ 3. **Debounce resize handlers.** Wait 150ms after final resize event before recalculating layout-dependent values.
55
+
56
+ 4. **Use `passive: true` on all scroll listeners.** Required for scroll-linked effects. Mandatory. No exceptions.
57
+
58
+ ---
59
+
60
+ ## 2. will-change Strategy
61
+
62
+ ### Rules
63
+
64
+ | Rule | Constraint | Violation Cost |
65
+ |------|-----------|----------------|
66
+ | Apply timing | 200ms BEFORE animation starts | Late = missed frames at animation start |
67
+ | Remove timing | 200ms AFTER animation ends | Early = frames dropped at end |
68
+ | Max simultaneous elements | 3 | Each promoted layer consumes 4-8MB GPU memory |
69
+ | Never apply globally | `* { will-change: transform }` is forbidden | Promotes every element to its own layer; GPU memory exhaustion |
70
+ | Never apply to text-only elements | No will-change on `<p>`, `<span>`, `<h1>` | Text layers are expensive; subpixel rendering breaks |
71
+ | Never apply permanently | Remove after animation completes | Each layer increases compositor tree traversal time |
72
+
73
+ ### Implementation
74
+
75
+ ```javascript
76
+ // Correct: toggle will-change around animation lifecycle
77
+ const element = document.querySelector('.parallax-layer');
78
+
79
+ // 200ms before animation
80
+ setTimeout(() => element.style.willChange = 'transform', 0);
81
+
82
+ // Animation runs
83
+
84
+ // 200ms after animation completes
85
+ animation.onComplete = () => {
86
+ setTimeout(() => element.style.willChange = 'auto', 200);
87
+ };
88
+ ```
89
+
90
+ ### Layer Count Budget
91
+
92
+ ```
93
+ Desktop (1920x1080, dedicated GPU): max 10 compositor layers per viewport
94
+ Tablet (768x1024, integrated GPU): max 6 compositor layers
95
+ Mobile (375x812, mobile GPU): max 4 compositor layers
96
+ Low-end (budget Android): max 2 compositor layers
97
+ ```
98
+
99
+ ### Layer Count Calculation
100
+
101
+ Each of the following counts as one compositor layer:
102
+ - Any element with `will-change: transform` or `will-change: opacity`
103
+ - Any element with `transform: translateZ(0)` or `translate3d()`
104
+ - Any element with `position: fixed`
105
+ - Any element with `opacity < 1`
106
+ - Any element with a 3D transform (even if no will-change)
107
+ - Any `<video>` element (always its own layer)
108
+ - Any element with `filter` applied
109
+ - The root layer (always counts as 1)
110
+
111
+ **Example:** A pinned hero with 5 parallax layers + 1 fixed title + 1 video background = 8 layers. Exceeds mobile budget of 4. Must degrade.
112
+
113
+ ---
114
+
115
+ ## 3. Mobile Degradation Matrix
116
+
117
+ ### Tier 1: Flagship (iPhone 15 Pro, Pixel 8 Pro, Samsung S24)
118
+
119
+ | Capability | Status |
120
+ |-----------|--------|
121
+ | Full parallax depth layers | All layers active |
122
+ | 3D transforms | Enabled |
123
+ | Filter animations (non-scroll) | Allowed |
124
+ | Video backgrounds | Allowed |
125
+ | Particle effects | Max 200 particles |
126
+ | Target frame rate | 60fps |
127
+ | Minimum acceptable | 55fps |
128
+
129
+ ### Tier 2: Mid-range (iPhone 12, Pixel 6, Samsung S21)
130
+
131
+ | Capability | Status |
132
+ |-----------|--------|
133
+ | Depth layers | Reduce to 70% of desktop count (round down) |
134
+ | 3D transforms | rotateY/Z allowed; perspective: 800px min |
135
+ | Filter animations | Disabled entirely |
136
+ | Video backgrounds | Poster image only, no autoplay |
137
+ | Particle effects | Max 50 particles |
138
+ | Target frame rate | 55fps |
139
+ | Minimum acceptable | 45fps |
140
+
141
+ ### Tier 3: Budget (iPhone SE 2020, Pixel 4a, budget Android)
142
+
143
+ | Capability | Status |
144
+ |-----------|--------|
145
+ | Depth layers | Max 2 layers (background + foreground) |
146
+ | 3D transforms | Disabled; fall back to 2D transforms |
147
+ | Parallax | Disabled; static backgrounds |
148
+ | Animations | Opacity transitions only |
149
+ | Particle effects | Disabled |
150
+ | Target frame rate | 30fps |
151
+ | Minimum acceptable | 24fps |
152
+
153
+ ### Tier 4: Reduced Motion (prefers-reduced-motion: reduce)
154
+
155
+ | Capability | Status |
156
+ |-----------|--------|
157
+ | All scroll-driven animation | Disabled or instant |
158
+ | Parallax | Disabled |
159
+ | Pinned sections | Convert to static flow layout |
160
+ | Transitions | Instant (0ms duration) |
161
+ | Content accessibility | All content visible without animation |
162
+ | Auto-playing elements | Disabled |
163
+ | Respect method | CSS media query + JS detection (both required) |
164
+
165
+ ```css
166
+ /* Mandatory reduced-motion support */
167
+ @media (prefers-reduced-motion: reduce) {
168
+ *, *::before, *::after {
169
+ animation-duration: 0.01ms !important;
170
+ animation-iteration-count: 1 !important;
171
+ transition-duration: 0.01ms !important;
172
+ scroll-behavior: auto !important;
173
+ }
174
+ }
175
+ ```
176
+
177
+ ```javascript
178
+ // JS detection (required in addition to CSS)
179
+ const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
180
+ if (prefersReducedMotion) {
181
+ // Disable GSAP ScrollTrigger pinning
182
+ // Disable parallax
183
+ // Show all content immediately
184
+ }
185
+ ```
186
+
187
+ ### Tier Detection
188
+
189
+ ```javascript
190
+ function getPerformanceTier() {
191
+ // Check reduced motion first (takes precedence)
192
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
193
+ return 'reduced';
194
+ }
195
+
196
+ // NOTE: deviceMemory is a coarse heuristic — caps at 8, unsupported in Safari/Firefox.
197
+ // Treat as a hint, not ground truth; fall back to cores + UA.
198
+ const memory = navigator.deviceMemory; // GB (Chromium only)
199
+ const cores = navigator.hardwareConcurrency;
200
+ const isMobile = /iPhone|iPad|iPod|Android/.test(navigator.userAgent);
201
+
202
+ if (!isMobile) return 'desktop';
203
+ if (memory >= 8 && cores >= 6) return 'flagship';
204
+ if (memory >= 4 && cores >= 4) return 'mid-range';
205
+ return 'budget';
206
+ }
207
+ ```
208
+
209
+ ---
210
+
211
+ ## 4. Benchmark Targets
212
+
213
+ ### Core Web Vitals
214
+
215
+ | Metric | Target | Maximum Acceptable | Measurement Tool |
216
+ |--------|--------|-------------------|------------------|
217
+ | First Contentful Paint (FCP) | < 1.0s | 1.5s | Lighthouse |
218
+ | Largest Contentful Paint (LCP) | < 1.8s | 2.5s | Lighthouse |
219
+ | Cumulative Layout Shift (CLS) | < 0.05 | 0.1 | Lighthouse |
220
+ | Total Blocking Time (TBT) | < 100ms | 200ms | Lighthouse |
221
+ | Time to Interactive (TTI) | < 2.5s | 3.8s | Lighthouse |
222
+ | Lighthouse Performance | > 95 | 90 | Lighthouse |
223
+
224
+ ### Scroll-Specific Metrics
225
+
226
+ | Metric | Target | Maximum Acceptable | Measurement Method |
227
+ |--------|--------|-------------------|-------------------|
228
+ | Scroll jank (% frames dropped) | < 2% | 5% | Chrome DevTools Performance panel |
229
+ | Scroll latency (input to paint) | < 8ms | 16ms | DevTools input timeline |
230
+ | Layer promotion time | < 30ms | 50ms | DevTools Layers panel |
231
+ | Compositor thread budget | < 4ms/frame | 6.67ms | DevTools Performance |
232
+ | Main thread idle during scroll | > 60% | 40% | DevTools Performance |
233
+
234
+ ### Scroll Jank Measurement Protocol
235
+
236
+ 1. Open Chrome DevTools > Performance panel
237
+ 2. Click Record
238
+ 3. Scroll the page at moderate speed for 10 seconds
239
+ 4. Stop recording
240
+ 5. Count frames exceeding 16.67ms (red bars in timeline)
241
+ 6. Formula: `(dropped_frames / total_frames) * 100`
242
+ 7. Result must be < 5% to ship
243
+
244
+ ### Device Testing Matrix
245
+
246
+ Test on ALL of the following before shipping:
247
+
248
+ | Device | OS | Browser | Test Focus |
249
+ |--------|-----|---------|-----------|
250
+ | iPhone 15 Pro | iOS 17 | Safari | Flagship baseline |
251
+ | iPhone 12 | iOS 17 | Safari | Mid-range tier |
252
+ | iPhone SE 2022 | iOS 17 | Safari | Budget tier |
253
+ | Samsung S24 | Android 14 | Chrome | Flagship Android |
254
+ | Pixel 6 | Android 14 | Chrome | Mid-range Android |
255
+ | Budget Android (Moto G) | Android 13 | Chrome | Budget tier |
256
+ | MacBook Pro M3 | macOS 14 | Chrome | Desktop reference |
257
+ | Windows laptop (i5) | Windows 11 | Chrome | Desktop mid-range |
258
+
259
+ ---
260
+
261
+ ## 5. Image & Asset Budget
262
+
263
+ ### Per Chapter / Section
264
+
265
+ | Metric | Desktop | Mobile | Enforcement |
266
+ |--------|---------|--------|-------------|
267
+ | Max images per chapter | 3 | 2 | Hard limit |
268
+ | Max image weight (total) | 500KB | 200KB | Hard limit |
269
+ | Max image dimensions | 1920px wide | 828px wide | Hard limit |
270
+ | Image format | WebP | WebP | Required |
271
+ | Fallback format | AVIF | AVIF | When browser support > 85% |
272
+ | Legacy fallback | JPEG | JPEG | For IE11 (if required) |
273
+
274
+ ### Image Loading Strategy
275
+
276
+ ```
277
+ Above the fold (first viewport):
278
+ - eager loading
279
+ - preload critical hero image in <head>
280
+ - max 2 images
281
+
282
+ Below the fold:
283
+ - loading="lazy" on all images
284
+ - IntersectionObserver rootMargin: "200px" (start load 200px before visible)
285
+ - decode="async" for non-critical images
286
+
287
+ During scroll animations:
288
+ - No network requests may fire
289
+ - All animation assets must be loaded before animation starts
290
+ ```
291
+
292
+ ### Image Preloading
293
+
294
+ ```html
295
+ <!-- Preload first viewport image only -->
296
+ <link rel="preload" as="image" href="hero.webp" type="image/webp">
297
+ ```
298
+
299
+ Preload **maximum 2 images** per page. Additional preloads consume bandwidth and delay first render.
300
+
301
+ ### Font Budget
302
+
303
+ | Metric | Limit | Rationale |
304
+ |--------|-------|-----------|
305
+ | Max font families | 2 | Each family = additional blocking request |
306
+ | Max weights per family | 4 | Common: 400, 500, 700, 400i |
307
+ | Font-display | swap | Mandatory. No exceptions. |
308
+ | Preload fonts | 1 | Only the first viewport's primary font |
309
+ | Font format | woff2 | With woff fallback for older browsers |
310
+ | Total font weight | < 200KB | All weights combined |
311
+
312
+ ```css
313
+ /* Mandatory font-display: swap */
314
+ @font-face {
315
+ font-family: 'Primary';
316
+ src: url('primary.woff2') format('woff2'),
317
+ url('primary.woff') format('woff');
318
+ font-weight: 400 700;
319
+ font-display: swap; /* Text visible immediately in fallback font */
320
+ }
321
+ ```
322
+
323
+ ### CSS Budget
324
+
325
+ | Metric | Limit |
326
+ |--------|-------|
327
+ | Critical CSS | < 20KB (inline in `<head>`) |
328
+ | Total CSS | < 100KB gzipped |
329
+ | Unused CSS | < 30% (measured via Coverage tab) |
330
+ | CSS animations | Prefer CSS keyframes over JS for simple effects |
331
+
332
+ ### JavaScript Budget
333
+
334
+ | Metric | Limit |
335
+ |--------|-------|
336
+ | Critical JS (blocking) | 0KB — all JS must be async or deferred |
337
+ | GSAP core | ~25KB gzipped (allowed) |
338
+ | ScrollTrigger plugin | ~8KB gzipped (allowed) |
339
+ | Animation-related JS total | < 100KB gzipped |
340
+ | Third-party scripts | Avoid during scroll animations |
341
+
342
+ ---
343
+
344
+ ## 6. Monitoring Checklist
345
+
346
+ ### Pre-Launch Checklist
347
+
348
+ Before shipping any scroll experience, ALL items must be checked:
349
+
350
+ - [ ] Chrome DevTools Performance: record 10s scroll, verify < 5% red frames
351
+ - [ ] Lighthouse: Performance score > 90
352
+ - [ ] WebPageTest: Filmstrip shows smooth visual progression during scroll
353
+ - [ ] iPhone 12 (Safari): no visible stutter during fast scroll (flick gesture)
354
+ - [ ] iPhone SE: content is accessible, no broken layout on budget tier
355
+ - [ ] Reduced-motion test: all content visible, no broken layout with `prefers-reduced-motion: reduce`
356
+ - [ ] Battery test: 5 minutes of continuous scrolling drains < 3% battery
357
+ - [ ] Memory test: tab memory does not grow > 50MB after 5 minutes of scrolling
358
+ - [ ] Layer count: DevTools Layers panel shows < 10 layers on desktop, < 4 on mobile
359
+ - [ ] No layout thrashing: DevTools Performance shows no purple "Layout" bars during scroll
360
+ - [ ] Network: no images load during scroll animation (all preloaded)
361
+
362
+ ### CI Integration
363
+
364
+ ```javascript
365
+ // lighthouse-ci.config.js
366
+ module.exports = {
367
+ ci: {
368
+ assert: {
369
+ assertions: {
370
+ 'categories:performance': ['error', { minScore: 0.90 }],
371
+ 'categories:accessibility': ['error', { minScore: 0.95 }],
372
+ 'first-contentful-paint': ['error', { maxNumericValue: 1500 }],
373
+ 'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
374
+ 'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
375
+ 'total-blocking-time': ['error', { maxNumericValue: 200 }],
376
+ },
377
+ },
378
+ },
379
+ };
380
+ ```
381
+
382
+ ### Regression Thresholds
383
+
384
+ | Scenario | Action |
385
+ |----------|--------|
386
+ | Lighthouse score drops below 90 | Block merge |
387
+ | Scroll jank exceeds 5% | Block merge |
388
+ | LCP exceeds 2.5s | Block merge |
389
+ | Layer count exceeds budget | Block merge |
390
+ | Bundle size exceeds 100KB | Flag for review (not blocking) |
391
+
392
+ ---
393
+
394
+ ## 7. GSAP-Specific Rules
395
+
396
+ ### ScrollTrigger Configuration
397
+
398
+ ```javascript
399
+ // Mandatory defaults for all ScrollTrigger instances
400
+ ScrollTrigger.defaults({
401
+ markers: false, // Never ship with markers enabled
402
+ scrub: 0.5, // 0.5s smoothing lag (smooth but responsive)
403
+ invalidateOnRefresh: true, // Recalculate on resize
404
+ fastScrollEnd: true, // Prevent animation catch-up on fast scroll
405
+ preventOverlaps: true, // Prevent tween conflicts
406
+ });
407
+ ```
408
+
409
+ ### Performance Rules
410
+
411
+ | Rule | Constraint |
412
+ |------|-----------|
413
+ | Max concurrent ScrollTriggers | 50 per page |
414
+ | Scrub smoothing | 0.3-0.8 range. Never 0 (choppy) or > 1 (sluggish). |
415
+ | Pin spacing | Use `pinSpacing: true` (default). Prevents layout collapse. |
416
+ | Batch tweens | Use `gsap.timeline()` for sequences, not individual tweens. |
417
+ | Kill on unmount | Call `ScrollTrigger.kill()` on all instances when section removed. |
418
+ | No layout reads in onUpdate | Cache values. No `getBoundingClientRect`. |
419
+
420
+ ### Forbidden GSAP Patterns
421
+
422
+ ```javascript
423
+ // WRONG: Individual tweens for each element
424
+ elements.forEach(el => {
425
+ gsap.to(el, { y: 100, scrollTrigger: { trigger: el } });
426
+ });
427
+
428
+ // RIGHT: Single timeline with scrub
429
+ const tl = gsap.timeline({
430
+ scrollTrigger: {
431
+ trigger: '.container',
432
+ scrub: 0.5,
433
+ }
434
+ });
435
+ tl.to('.el1', { y: 100 })
436
+ .to('.el2', { y: 200 }, '<');
437
+
438
+ // WRONG: Reading layout in onUpdate
439
+ scrollTrigger: {
440
+ onUpdate: () => {
441
+ const rect = el.getBoundingClientRect(); // FORBIDDEN
442
+ }
443
+ }
444
+
445
+ // RIGHT: Cached values
446
+ const cachedY = el.offsetTop; // Read once on init
447
+ scrollTrigger: {
448
+ onUpdate: (self) => {
449
+ const y = cachedY * self.progress; // Use cached value
450
+ }
451
+ }
452
+ ```
453
+
454
+ ---
455
+
456
+ ## 8. Failure Modes
457
+
458
+ ### What Happens When Constraints Are Violated
459
+
460
+ | Constraint Violated | Symptom | User Impact |
461
+ |-------------------|---------|-------------|
462
+ | > 10 compositor layers | GPU memory exhaustion, tab crash | Page goes blank, browser kills tab |
463
+ | Layout animation during scroll | Purple bars in DevTools | Janky, stuttering scroll |
464
+ | Filter animation during scroll | Main-thread compositing | 15-30fps, unusable |
465
+ | No `passive: true` on scroll | Blocked touch events | Scroll doesn't respond to touch |
466
+ | will-change on > 3 elements | Excessive GPU memory | Tab crashes on mobile |
467
+ | Images loading during scroll | Network waterfall in Performance | Scroll pauses while images decode |
468
+ | > 2ms scroll handler | Input latency | Scroll feels "disconnected" from finger |
469
+ | No reduced-motion support | Motion sickness, inaccessible content | Legal/compliance risk |
470
+
471
+ ### Emergency Degradation
472
+
473
+ If runtime frame rate drops below target tier's minimum for > 3 seconds:
474
+
475
+ 1. Disable all parallax immediately
476
+ 2. Reduce to opacity-only transitions
477
+ 3. Unpin all pinned sections
478
+ 4. Log degradation event to analytics
479
+ 5. Do not re-enable effects without page reload
480
+
481
+ ```javascript
482
+ let consecutiveSlowFrames = 0;
483
+
484
+ function checkFrameRate(timestamp) {
485
+ if (lastTimestamp) {
486
+ const delta = timestamp - lastTimestamp;
487
+ if (delta > 33.33) { // Below 30fps
488
+ consecutiveSlowFrames++;
489
+ if (consecutiveSlowFrames > 3 * 60) { // 3 seconds at 60fps sample
490
+ emergencyDegrade();
491
+ }
492
+ } else {
493
+ consecutiveSlowFrames = 0;
494
+ }
495
+ }
496
+ lastTimestamp = timestamp;
497
+ requestAnimationFrame(checkFrameRate);
498
+ }
499
+ ```