Spurlock Studios
Contact
Motion That Survives Production

There is a category of website that looks extraordinary in a portfolio video and falls apart the moment it meets a three-year-old phone on a hotel wifi. I have shipped enough motion-heavy sites to know exactly which decisions cause that, because I made most of them at least once.

Only two properties are cheap

transform and opacity are composited. Everything else is not. Animating width, top, box-shadow, filter or background-position forces layout or paint on every frame, and on a mid-range device you will feel it immediately.

This is not a nuance, it is the whole game. If a motion idea cannot be expressed as transform and opacity, the idea needs to change — not the budget.

The one exception I allow is a filter: blur() on a fixed, small overlay element that animates rarely. Grain qualifies. A blurred hero that animates on scroll does not.

will-change is a loan, not a gift

Putting will-change: transform on everything promotes everything to its own compositor layer, and enough layers will exhaust memory on exactly the devices you were trying to help.

Set it when a tween starts, remove it when the tween ends. GSAP does this correctly by default. Manual CSS usually does not.

Kill your ScrollTriggers

Every pinned section, every scrubbed timeline, every containerAnimation holds references. In an SPA or a framework with client-side routing, an un-killed ScrollTrigger on an unmounted component is a memory leak that also fires callbacks against detached DOM nodes.

useEffect(() => {
  const ctx = gsap.context(() => {
    /* all triggers created in here */
  }, rootRef);
  return () => ctx.revert();
}, []);

gsap.context plus revert in cleanup handles this completely. There is no reason to do it any other way.

The hero must work before the motion loads

The most damaging performance mistake is gating content on the animation system. If your headline is opacity: 0 in CSS and only becomes visible when a GSAP timeline runs, then a slow bundle, a failed CDN, or a JavaScript error produces a blank page.

Ship the hero in its final state and let the motion system take over. Under prefers-reduced-motion the static version is the deliverable, which means you have to build it anyway.

Reduced motion is not a fallback

Treating reduced motion as a degraded experience is how you end up with a version nobody tested. gsap.matchMedia lets you author it as a first-class variant:

const mm = gsap.matchMedia();

mm.add("(prefers-reduced-motion: reduce)", () => {
  gsap.set(targets, { clearProps: "all", opacity: 1 });
});

mm.add("(prefers-reduced-motion: no-preference)", () => {
  /* pins, scrubs, trails */
});

Two authored experiences, one codebase, and the accessible one is not an afterthought that broke six deploys ago.

What “100 Lighthouse” actually requires

Static output, no render-blocking JavaScript, fonts preconnected and swapped, images sized and modern-format, and interactive components hydrated on visibility rather than on load. The motion budget comes after all of that is satisfied — which is the opposite of how most sites are built, and the reason most cinematic sites score in the sixties.

Start a sprint