Same workload, three engines — same frames, 15× less JavaScript

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.

Reference run (Apple-silicon Mac, headless Chrome via CDP — replace it with yours above)

engineelementsfpsp95frames >25ms
scrollvars9006017.6 ms0 / 721
gsap + ScrollTrigger9006017.6 ms0 / 720
framer-motion (React)90059.917.6 ms1 / 720

The CPU cost for the same frames — measured, reproducible

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.

engineJS scriptstyle recalclayouttask totalJS heapfps
scrollvars100 ms195 ms7 ms958 ms1.4 MB60
gsap + ScrollTrigger (idiomatic, 1 trigger/box)233 ms85 ms7 ms904 ms6.2 MB60
gsap + ScrollTrigger (batched, 1 trigger/section)175 ms86 ms7 ms993 ms6.7 MB60
framer-motion (React)740 ms48 ms8 ms1523 ms11.1 MB60

The style-recalc curve (the honest cost of the CSS-variable mechanism)

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 sizescrollvars recalcgsap-batched recalcscrollvars scriptgsap-batched scriptsv heapgsap heap
5 nodes/box288 ms73 ms98 ms153 ms1.1 MB3.2 MB
20 nodes/box516 ms98 ms95 ms204 ms1 MB3.3 MB
50 nodes/box769 ms96 ms87 ms192 ms1.1 MB3.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