Scroll animation: cost, frame delivery and bundle size

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.

Total CPU and frame delivery

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.

enginetotal CPU (12 s)fpsJS scriptstyle recalclayoutJS heapp95worstframes >25ms
ScrollVars679 ms6059 ms137 ms (131–143)0 ms1 MB16.7 ms16.80
ScrollVars + styles/scoped.css766 ms6046 ms188 ms (166–235)0 ms1 MB16.7 ms16.80
ScrollVars (document variables published)3791 ms59.349 ms2604 ms (2203–3106)0 ms1.1 MB16.7 ms133.42
gsap + ScrollTrigger (idiomatic, 1 trigger/box)659 ms60145 ms57 ms (47–64)0 ms6.1 MB16.7 ms16.80
gsap + ScrollTrigger (batched, 1 trigger/section)626 ms60117 ms59 ms (47–65)0 ms6.8 MB16.8 ms16.80
framer-motion (React)1078 ms60490 ms42 ms (35–42)0 ms10.9 MB16.7 ms16.80

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

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 sizeenginestyle recalcJS scripttask totalheapfps
5 nodes/boxScrollVars146 ms (146–173)51 ms514 ms1 MB60
5 nodes/boxScrollVars + styles/scoped.css116 ms (89–132)48 ms442 ms1 MB60
5 nodes/boxScrollVars (page outputs off)140 ms (109–162)50 ms490 ms1 MB60
5 nodes/boxgsap + ScrollTrigger (batched, 1 trigger/section)51 ms (35–60)101 ms553 ms3.1 MB60
20 nodes/boxScrollVars158 ms (154–162)41 ms499 ms1 MB60
20 nodes/boxScrollVars + styles/scoped.css108 ms (94–126)41 ms437 ms1.1 MB60
20 nodes/boxScrollVars (page outputs off)164 ms (164–177)43 ms517 ms1 MB60
20 nodes/boxgsap + ScrollTrigger (batched, 1 trigger/section)33 ms (24–34)69 ms379 ms3.2 MB60
50 nodes/boxScrollVars269 ms (259–464)41 ms653 ms1 MB60
50 nodes/boxScrollVars + styles/scoped.css117 ms (111–253)41 ms471 ms1.1 MB60
50 nodes/boxScrollVars (page outputs off)266 ms (237–283)45 ms652 ms0.9 MB60
50 nodes/boxgsap + ScrollTrigger (batched, 1 trigger/section)32 ms (27–34)68 ms380 ms3.4 MB60

Low-end profile (4× synthetic CPU throttle; not a physical phone)

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.

engineJS scriptstyle recalctask totalfpsp95 frame
ScrollVars86 ms428 ms (244–690)1735 ms6018.4 ms
ScrollVars + styles/scoped.css85 ms646 ms (404–913)2052 ms6018.5 ms
ScrollVars (document variables published)138 ms9011 ms (8586–9623)10743 ms6018.5 ms
gsap + ScrollTrigger (idiomatic, 1 trigger/box)390 ms152 ms (73–208)1607 ms6018.3 ms
gsap + ScrollTrigger (batched, 1 trigger/section)254 ms149 ms (134–162)1519 ms6018.4 ms
framer-motion (React)1835 ms145 ms (108–160)3232 ms6018.4 ms

Raw: results/throttled-4x.json.

An app-shaped page (what the library adds to a page that lives)

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.

pagetask totalJS scriptstyle recalcfpsp95 frameframes >25ms
the page without the library126.8 ms (114.9–146)27.4 ms3.4 ms (3.4–4.2)6016.7 ms0
ScrollVars331.9 ms (320.2–574.6)51.8 ms80.4 ms (76.3–82.5)6016.7 ms0
ScrollVars + styles/scoped.css299 ms (255.9–340.3)51.4 ms56 ms (46.2–62.9)6016.8 ms0

Raw: results/app-shaped.json.

Real gallery sections (page outputs off)

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.

sectionJSstyletask totalp95worstframes >25ms
hero-cinematic42 ms100 ms (72–164)302 ms16.8 ms16.8 ms0
timeline-scrub35 ms60 ms (57–60)196 ms16.7 ms16.8 ms0
sticky-steps63 ms92 ms (83–97)322 ms16.8 ms16.8 ms0
stats-countup37 ms23 ms (21–36)210 ms16.7 ms16.8 ms0
case-study-rail35 ms49 ms (49–50)173 ms16.8 ms16.8 ms0
editorial-manifesto47 ms46 ms (45–52)194 ms16.7 ms16.8 ms0

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