Matching boxes and scroll progression across three engines, including idiomatic and batched GSAP. The interactive test adds synthetic app work to probe headroom. A steady average FPS does not exclude expensive frames. Keep the tab visible. CPU throttling is a synthetic experiment, not a substitute for testing the client's phone.
| engine | elements | bundle (gzip) | avg frame | fps | p95 | worst | frames >25ms | JS heap |
|---|
Three ScrollVars rows. The plain one is the default: the document outputs
--sv-page/--sv-v publish only when a stylesheet reads them, and nothing here does.
The styles/scoped.css row is the opt-in that stops the clocks inheriting (README, Scoped clocks),
run with the two author lines its contract asks of this fixture: it pays on this flat profile and wins on the
deep one below. The "document variables published" row is what the two document outputs cost when a page
reads them, not a competitor comparison. Use the batched GSAP row when comparing one tracker per section.
Small bundles do not guarantee lower CPU.
Measured by the committed harness (demo/bench/harness. Clone the repo,
cd demo/bench/harness && npm i && npm run measure): Chrome 152.0.7977.85,
median of 3 runs, engine order rotated per repetition.
Measured 2026-09-10T23:55:45.351Z; package 1.16.1, commit 24b4f2020eac7387c9cc82d9477abd0c0a9231ef. Viewport 800×600. CPU totals accumulate over 12 seconds; they are not per-frame times. Startup is separate; scroll intervals are never filtered. Style cells show median (min–max). Raw runs and source hashes: results/latest.json.
| engine | total CPU (12 s) | fps | JS script | style recalc | layout | JS heap | p95 | worst | frames >25ms |
|---|---|---|---|---|---|---|---|---|---|
| ScrollVars | 679 ms | 60 | 59 ms | 137 ms (131–143) | 0 ms | 1 MB | 16.7 ms | 16.8 | 0 |
| ScrollVars + styles/scoped.css | 766 ms | 60 | 46 ms | 188 ms (166–235) | 0 ms | 1 MB | 16.7 ms | 16.8 | 0 |
| ScrollVars (document variables published) | 3791 ms | 59.3 | 49 ms | 2604 ms (2203–3106) | 0 ms | 1.1 MB | 16.7 ms | 133.4 | 2 |
| gsap + ScrollTrigger (idiomatic, 1 trigger/box) | 659 ms | 60 | 145 ms | 57 ms (47–64) | 0 ms | 6.1 MB | 16.7 ms | 16.8 | 0 |
| gsap + ScrollTrigger (batched, 1 trigger/section) | 626 ms | 60 | 117 ms | 59 ms (47–65) | 0 ms | 6.8 MB | 16.8 ms | 16.8 | 0 |
| framer-motion (React) | 1078 ms | 60 | 490 ms | 42 ms (35–42) | 0 ms | 10.9 MB | 16.7 ms | 16.8 | 0 |
Every box gets a realistic subtree (?deep=N spans with distinct
selectors). A GSAP tween writes a transform on the box, so its cost does not depend on what the box contains; a custom property inherits, so every write re-resolves the box's whole subtree, and styles/scoped.css stops that at the readers. Compare document-wide writes, local writes and batched GSAP:
150 boxes, medians. Historical snapshot: 2026-09-10T23:55:45.351Z, package 1.16.1; raw results. A newer main comparison does not update this snapshot.
| subtree size | engine | style recalc | JS script | task total | heap | fps |
|---|---|---|---|---|---|---|
| 5 nodes/box | ScrollVars | 146 ms (146–173) | 51 ms | 514 ms | 1 MB | 60 |
| 5 nodes/box | ScrollVars + styles/scoped.css | 116 ms (89–132) | 48 ms | 442 ms | 1 MB | 60 |
| 5 nodes/box | ScrollVars (page outputs off) | 140 ms (109–162) | 50 ms | 490 ms | 1 MB | 60 |
| 5 nodes/box | gsap + ScrollTrigger (batched, 1 trigger/section) | 51 ms (35–60) | 101 ms | 553 ms | 3.1 MB | 60 |
| 20 nodes/box | ScrollVars | 158 ms (154–162) | 41 ms | 499 ms | 1 MB | 60 |
| 20 nodes/box | ScrollVars + styles/scoped.css | 108 ms (94–126) | 41 ms | 437 ms | 1.1 MB | 60 |
| 20 nodes/box | ScrollVars (page outputs off) | 164 ms (164–177) | 43 ms | 517 ms | 1 MB | 60 |
| 20 nodes/box | gsap + ScrollTrigger (batched, 1 trigger/section) | 33 ms (24–34) | 69 ms | 379 ms | 3.2 MB | 60 |
| 50 nodes/box | ScrollVars | 269 ms (259–464) | 41 ms | 653 ms | 1 MB | 60 |
| 50 nodes/box | ScrollVars + styles/scoped.css | 117 ms (111–253) | 41 ms | 471 ms | 1.1 MB | 60 |
| 50 nodes/box | ScrollVars (page outputs off) | 266 ms (237–283) | 45 ms | 652 ms | 0.9 MB | 60 |
| 50 nodes/box | gsap + ScrollTrigger (batched, 1 trigger/section) | 32 ms (27–34) | 68 ms | 380 ms | 3.4 MB | 60 |
Measured 2026-09-11T04:00:01.804Z; package 1.16.1, 5 runs. Headed Chrome; compare engines within this profile, not as a multiplier of the headless table. Fixed-work calibration ratios are in the raw results.
| engine | JS script | style recalc | task total | fps | p95 frame |
|---|---|---|---|---|---|
| ScrollVars | 86 ms | 428 ms (244–690) | 1735 ms | 60 | 18.4 ms |
| ScrollVars + styles/scoped.css | 85 ms | 646 ms (404–913) | 2052 ms | 60 | 18.5 ms |
| ScrollVars (document variables published) | 138 ms | 9011 ms (8586–9623) | 10743 ms | 60 | 18.5 ms |
| gsap + ScrollTrigger (idiomatic, 1 trigger/box) | 390 ms | 152 ms (73–208) | 1607 ms | 60 | 18.3 ms |
| gsap + ScrollTrigger (batched, 1 trigger/section) | 254 ms | 149 ms (134–162) | 1519 ms | 60 | 18.4 ms |
| framer-motion (React) | 1835 ms | 145 ms (108–160) | 3232 ms | 60 | 18.4 ms |
Raw: results/throttled-4x.json.
Measured 2026-09-11T04:08:34.653Z, 3 runs, order rotated. Generated by harness/app-shaped.mjs: a sticky header, a hero with entrance presets, a card grid with stagger, a parallax band, a feed of forty tracked items, a stream that mounts and unmounts rows every 250ms while the page scrolls, a route change at six seconds that replaces the grid and the feed, and a 5000-rule stylesheet. Same 12-second scroll as the tables above; the difference between the first row and the others is what the library costs here, animations included. The scoped row is gated: it rendered the same as the default at every sampled scroll position before its time counted.
| page | task total | JS script | style recalc | fps | p95 frame | frames >25ms |
|---|---|---|---|---|---|---|
| the page without the library | 126.8 ms (114.9–146) | 27.4 ms | 3.4 ms (3.4–4.2) | 60 | 16.7 ms | 0 |
| ScrollVars | 331.9 ms (320.2–574.6) | 51.8 ms | 80.4 ms (76.3–82.5) | 60 | 16.7 ms | 0 |
| ScrollVars + styles/scoped.css | 299 ms (255.9–340.3) | 51.4 ms | 56 ms (46.2–62.9) | 60 | 16.8 ms | 0 |
Raw: results/app-shaped.json.
Each complete generated page at 800×600, including gallery UI and any responsive fit-to-flow fallback. These are workload checks, not competitor comparisons or device guarantees. Historical snapshot: 2026-09-10T23:55:45.351Z, package 1.16.1; raw results.
| section | JS | style | task total | p95 | worst | frames >25ms |
|---|---|---|---|---|---|---|
| hero-cinematic | 42 ms | 100 ms (72–164) | 302 ms | 16.8 ms | 16.8 ms | 0 |
| timeline-scrub | 35 ms | 60 ms (57–60) | 196 ms | 16.7 ms | 16.8 ms | 0 |
| sticky-steps | 63 ms | 92 ms (83–97) | 322 ms | 16.8 ms | 16.8 ms | 0 |
| stats-countup | 37 ms | 23 ms (21–36) | 210 ms | 16.7 ms | 16.8 ms | 0 |
| case-study-rail | 35 ms | 49 ms (49–50) | 173 ms | 16.8 ms | 16.8 ms | 0 |
| editorial-manifesto | 47 ms | 46 ms (45–52) | 194 ms | 16.7 ms | 16.8 ms | 0 |
Inherited variables can make large subtrees expensive. Keep continuous clocks close to their consumers; prefer native view timelines for compatible effects. Interpret script, style, total work and frame stalls together.
Methodology: shared 12-second scroll clock; identical movement direction and opacity progression. CPU metrics use start/end deltas; startup is recorded separately. All frame intervals are retained. Raw JSON includes individual runs, version, hashes and fixed-work throttle ratios. The gallery rows measure real sections. Bundle sizes for competitors refer to their pinned public builds; React is additional for Framer. Sources: ScrollVars · gsap · gsap batched · framer-motion