Scroll-scrubbed elements (translate + opacity), scroll driven top→bottom→top over 12 s — three passes: 150 elements, 900, and 900 with 12 ms of synthetic "app work" burned per frame — the headroom test. Frame delivery is flat until the main thread saturates, so on fast machines every engine shows 60 fps; the last pass adds synthetic app work to probe headroom. On capable hardware all three engines deliver the same frames — that is the finding: the 46 KB engines buy no extra performance here, they buy API surface. The differences that persist are the bundle column, no-JS/SSR behavior, and cost on weak devices (to simulate one: DevTools → Performance → CPU 6× slowdown, then run again). Each engine runs in sequence below; frame times are measured identically. Keep this tab visible — browsers freeze background tabs.
| engine | elements | bundle (gzip) | avg frame | fps | p95 | worst | frames >25ms | JS heap |
|---|
| engine | elements | fps | p95 | frames >25ms |
|---|---|---|---|---|
| scrollvars | 900 | 60 | 17.6 ms | 0 / 721 |
| gsap + ScrollTrigger | 900 | 60 | 17.6 ms | 0 / 720 |
| framer-motion (React) | 900 | 59.9 | 17.6 ms | 1 / 720 |
The claim, stated precisely: scrollvars does not promise faster frames — frame delivery ties at 60 fps in every row, because every competent engine animates only the viewport. The claim is: the same frames for a ~12× smaller bundle, a fraction of the heap, zero React re-renders, and a fail-visible no-JS story — with total CPU trading blows (scrollvars wins the shallow scenarios, batched GSAP wins deep subtrees; both are below). Both GSAP variants are included: the idiomatic one-trigger-per-box page people actually write, and the batched expert build (one trigger per section), symmetric to scrollvars' one-tracker-per-section — compare against the batched row for the fair fight.
Measured by the committed harness (demo/bench/harness — clone the repo,
npm i && npm run measure): headless Chrome Chrome/152.0.7977.64,
median of 5 runs, engine order rotated per repetition.
Raw output: results/latest.json.
| engine | JS script | style recalc | layout | task total | JS heap | fps |
|---|---|---|---|---|---|---|
| scrollvars | 100 ms | 195 ms | 7 ms | 958 ms | 1.4 MB | 60 |
| gsap + ScrollTrigger (idiomatic, 1 trigger/box) | 233 ms | 85 ms | 7 ms | 904 ms | 6.2 MB | 60 |
| gsap + ScrollTrigger (batched, 1 trigger/section) | 175 ms | 86 ms | 7 ms | 993 ms | 6.7 MB | 60 |
| framer-motion (React) | 740 ms | 48 ms | 8 ms | 1523 ms | 11.1 MB | 60 |
Every box gets a realistic subtree (?deep=N spans with distinct
selectors); vars written on the section invalidate it all. scrollvars vs the batched GSAP
build, 150 boxes, medians:
| subtree size | scrollvars recalc | gsap-batched recalc | scrollvars script | gsap-batched script | sv heap | gsap heap |
|---|---|---|---|---|---|---|
| 5 nodes/box | 288 ms | 73 ms | 98 ms | 153 ms | 1.1 MB | 3.2 MB |
| 20 nodes/box | 516 ms | 98 ms | 95 ms | 204 ms | 1 MB | 3.3 MB |
| 50 nodes/box | 769 ms | 96 ms | 87 ms | 192 ms | 1.1 MB | 3.4 MB |
Read the numbers honestly: scrollvars wins JS script and heap in both pairings and posts the worst style-recalc — an inherited custom property pays per-descendant on every change, a direct transform write pays per-element. The curve above is that cost growing with subtree size, and at the deep end the batched GSAP build wins total CPU. The authoring rule that keeps you on the cheap side: keep tracked elements thin — big static content lives next to the animated elements, not inside them (the var then never inherits through it). It is cheap, not free, and now it is measured.
Stress test (not a typical page): Lighthouse mobile (Moto G emulation, load-only) on the 900-element pages: scrollvars 100 · framer-motion 94 (LCP 2.8 s) · idiomatic GSAP 88 (TBT 230 ms — building 900 triggers on a throttled main thread). No real marketing page ships 900 individual triggers; the point is the shape of the load cost, not the delta you should expect on your page.
Methodology: identical DOM and scroll driver in every page; the only variable is the animation engine. The harness (demo/bench/harness) serves these exact pages,
rotates engine order, uses CDP Performance.getMetrics, and verifies any CPU
throttle with a calibration spin. Bundle sizes: competitors from their public CDN builds;
scrollvars via esbuild+gzip of its own source, inlined in its harness (framer-motion
additionally needs React, not counted for it). Run it on your own machine — that’s the point.
Sources: scrollvars ·
gsap ·
gsap batched ·
framer-motion