GSAP scroll animations without the jank
Pinning, batching, and the will-change rules I follow to keep ScrollTrigger buttery — even on a mid-range Android.
Scroll animation goes wrong in a predictable way: it looks perfect on the laptop you built it on and stutters on everything else. Keeping ScrollTrigger smooth is mostly about animating the right properties and letting the browser do less work per frame.
01Only animate transform and opacity
The single biggest win. transform and opacity are composited on the GPU and
never trigger layout or paint. Animating top, height, or margin forces a
reflow on every frame, which is where the jank comes from.
// Cheap: composited, no layout.
gsap.to(el, { y: 0, opacity: 1, duration: 0.6, ease: "smooth" });
// Expensive: reflows every frame. Avoid.
gsap.to(el, { top: 0, height: "auto" });
02Run once, then get out of the way
Reveal-on-scroll should fire a single time and stop listening. A trigger that re-runs every time an element re-enters the viewport is both distracting and wasteful.
- Use
once: trueso the trigger kills itself after firing. - Set the start generously (
top 88%) so it reveals before the reader arrives. - Respect
prefers-reduced-motionand skip the whole thing when it's set.
Refresh after the DOM settles
ScrollTrigger measures positions once. If content mounts or resizes after that — fonts loading, images arriving, a route swap — the triggers point at stale coordinates. One call fixes it:
ScrollTrigger.refresh();
03Batch, don't spawn hundreds of triggers
A list of forty cards does not need forty triggers. ScrollTrigger.batch()
groups elements and staggers them with a fraction of the overhead.
Smooth motion isn't about doing more on scroll. It's about doing as little as possible, on the cheapest properties, exactly once.
Follow those four rules and the mid-range Android stops being the device that embarrasses your demo.