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.
- package/COMPATIBILITY.md +244 -0
- package/LICENSE +21 -0
- package/MODELS.md +92 -0
- package/README.md +250 -0
- package/SKILL.md +1003 -0
- package/audit-mode.md +497 -0
- package/bin/install.mjs +91 -0
- package/compile-choreography.mjs +296 -0
- package/decision-log.md +241 -0
- package/examples/GETTING_STARTED.md +279 -0
- package/examples/KNOWN_ISSUES.md +50 -0
- package/examples/PROMPTS.md +166 -0
- package/examples/luxe/README.md +88 -0
- package/examples/luxe/index.html +662 -0
- package/examples/noir/README.md +72 -0
- package/examples/noir/index.html +634 -0
- package/examples/pop/README.md +81 -0
- package/examples/pop/index.html +711 -0
- package/examples/renaissance/README.md +39 -0
- package/examples/renaissance/index.html +648 -0
- package/examples/studio/README.md +77 -0
- package/examples/studio/chapters.js +105 -0
- package/examples/studio/index.html +520 -0
- package/manifest.json +92 -0
- package/manifest.md +136 -0
- package/package.json +56 -0
- package/references/film-archetypes.md +211 -0
- package/references/performance-budget.md +499 -0
- package/references/scroll-patterns.md +693 -0
- package/scroll-choreography-compilation.md +543 -0
- package/scroll-choreography.json +1512 -0
- package/taste-guardrails.md +164 -0
- package/templates/nextjs/.env.example +41 -0
- package/templates/nextjs/app/api/fal/proxy/route.ts +33 -0
- package/templates/nextjs/app/api/fal/webhook/route.ts +132 -0
- package/templates/nextjs/app/api/generate-edition-asset/route.ts +66 -0
- package/templates/nextjs/app/globals.css +80 -0
- package/templates/nextjs/app/layout.tsx +21 -0
- package/templates/nextjs/app/page.tsx +10 -0
- package/templates/nextjs/components/ChapterDemoVisual.tsx +212 -0
- package/templates/nextjs/components/ChapterScene.tsx +373 -0
- package/templates/nextjs/components/EditionsPage.tsx +116 -0
- package/templates/nextjs/components/SmoothScrollProvider.tsx +8 -0
- package/templates/nextjs/lib/api-guard.ts +110 -0
- package/templates/nextjs/lib/editions-manifest.ts +224 -0
- package/templates/nextjs/lib/fal-client.ts +12 -0
- package/templates/nextjs/lib/fal-generate.ts +86 -0
- package/templates/nextjs/lib/fal-models.ts +213 -0
- package/templates/nextjs/lib/prompt-contract.ts +97 -0
- package/templates/nextjs/lib/use-device.ts +42 -0
- package/templates/nextjs/lib/use-lenis.ts +35 -0
- package/templates/nextjs/next.config.ts +29 -0
- package/templates/nextjs/package-lock.json +6455 -0
- package/templates/nextjs/package.json +41 -0
- package/templates/nextjs/package.patch.json +28 -0
- package/templates/nextjs/postcss.config.js +6 -0
- package/templates/nextjs/scripts/generate-chapter-assets.mjs +243 -0
- package/templates/nextjs/scripts/setup.mjs +170 -0
- package/templates/nextjs/tailwind.config.ts +37 -0
- package/templates/nextjs/tsconfig.json +23 -0
- 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
|
+
```
|