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:

  1. 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.
  2. Every media box has explicit dimensions. width and height attributes on images, an aspect-ratio on 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" or defer so 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 transform and opacity only. Both are compositor-friendly. Animating top, left, width or height forces 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 call preventDefault.
  • Batch measurement. ScrollTrigger caches positions and recalculates on resize rather than per frame, let it, instead of measuring yourself.
  • Use will-change sparingly 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.

Related articles

Let's grow together

Ready to take your digital presence to the next level?

Tell us where you want to grow. We will map the strategy, build it and measure what it returns.