Website & App Development
Heavy Scroll Animation Without Wrecking Core Web Vitals
Scroll-driven motion and a 90+ PageSpeed score are not mutually exclusive. The conflict is almost always about what runs before first paint, not how much motion there is.

Agency sites are usually a fight between two goals. The design wants pinned sections, parallax, text that reveals line by line. The performance budget wants almost no JavaScript. The usual outcome is a beautiful site that scores 54 on mobile.
That trade-off is mostly avoidable. In our experience the damage comes from a handful of specific decisions, not from the quantity of animation.
CLS: reserve the space before you animate into it
The most common cause of layout shift on an animated site is content that starts at opacity: 0 and gets moved into place by JavaScript. If the animation library loads late, the browser has already painted a page with a hole in it, and filling that hole is a shift.
Two rules keep this clean:
- The resting state in your HTML and CSS is the finished state. An element renders visible and correctly positioned with no JavaScript at all. Motion is something JS opts an element into, never something it rescues an element from.
- Every media box has explicit dimensions.
widthandheightattributes on images, anaspect-ratioon video and embed containers. This costs nothing and eliminates an entire class of shift.
The second rule also means a failed script, a slow network, or a reduced-motion preference all degrade to the same safe result: a static, complete page.
LCP: keep the animation library off the critical path
Your largest contentful element is usually the hero headline or hero image. Nothing about animating it later should delay painting it now.
- Load animation code with
type="module"ordeferso it never blocks the parser. - Do not let the hero’s appearance depend on a library finishing initialisation. Render it, then enhance it.
- Self-host fonts, preload only the one or two faces used above the fold, and use
font-display: swap. Preloading eight weights means eight requests competing with your LCP image for bandwidth. - Defer anything WebGL until its section is actually near the viewport. A Three.js bundle parsed during initial load is one of the most expensive things you can do to a mobile score.
Astro’s islands model makes this mostly a matter of choosing the right directive. client:visible on a heavy component is the difference between a 3D accent that costs nothing at load and one that costs a second.
INP: stay off the main thread during scroll
Scroll-linked animation goes wrong when scroll handlers do layout work. Reading offsetTop or getBoundingClientRect() inside a scroll callback forces synchronous layout on every frame.
- Animate
transformandopacityonly. Both are compositor-friendly. Animatingtop,left,widthorheightforces layout on every frame and will show up as jank. - Register scroll listeners as
{ passive: true }so the browser does not wait to find out whether you will callpreventDefault. - Batch measurement. ScrollTrigger caches positions and recalculates on resize rather than per frame, let it, instead of measuring yourself.
- Use
will-changesparingly and remove it after the animation. Every promoted layer costs GPU memory, and a page full of them is slower, not faster.
Reduced motion is a code path, not a stylesheet
A media query can flatten CSS transitions to nothing. It cannot undo a GSAP timeline that has already set inline transforms on an element, because those inline styles win.
Honouring the preference properly means checking it in JavaScript before building timelines:
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduced) return; // never create the timeline
Combined with the first rule above, resting state is finished state, skipping initialisation leaves a complete, readable page rather than an invisible one. This is also the reason the two rules are worth enforcing from the first component rather than retrofitting.
Measure on a throttled mobile profile
Desktop numbers on a fast machine will tell you everything is fine. Test with CPU throttling on, on a real mid-range Android if you can. Scroll-linked animation is one of the few things that behaves dramatically differently on constrained hardware, because it is competing for the main thread with everything else.
The short version
Heavy motion is not what costs you a good score. Blocking first paint costs you. Animating layout properties costs you. Hiding content until JavaScript rescues it costs you. Fix those three and you can keep the pinned sections, the parallax, and the reveals, and still pass Core Web Vitals on a mid-range phone.



