Skip to content

· 2 min read

The animation was on the critical path

This site's hero looked fast and measured slow. Largest Contentful Paint was waiting on the animation library. Moving one reveal from JavaScript to CSS took mobile performance from 78 to 97.

  • Performance
  • Engineering
  • Motion

The hero on this site reveals my name line by line, each line sliding up from behind a mask, followed by a short introduction. On a fast laptop it looks instant. On a mid-range phone, Lighthouse told a different story: a mobile performance score of 78 and a Largest Contentful Paint of 3.4 seconds.

The breakdown was the interesting part. Of those 3.4 seconds, almost 2.9 were element render delay. The server responded quickly and nothing was waiting on a download for the content itself. The largest element on the page was ready — and invisible.

Why it was invisible

The reveal was a GSAP timeline. Before it ran, the hero text sat at zero opacity or translated below its mask, so it could animate in. That meant the text could not become visible until the animation library had downloaded, parsed and executed.

On a throttled phone, that took most of three seconds. The largest paint on the page was gated on a JavaScript bundle whose only job was to make the text appear nicely.

The fix

The intro is a one-shot reveal. It does not depend on scrolling, user input or anything else that needs JavaScript to know. CSS can express it exactly.

So the load-in became CSS keyframe animations: the same durations, the same stagger between lines, and the same easing curve, translated from GSAP's expo.out to its equivalent cubic-bezier. The browser starts it on the first frame. GSAP still handles everything genuinely scroll-linked, like the name drifting as you scroll away.

Two smaller changes went with it. The hairline grid behind the hero had been drifting via an infinite GSAP tween — a permanent animation loop on the main thread, purely for decoration. That became a CSS animation too, which the browser can run on the compositor. And one section had been measuring its own size on every pointer movement, forcing a layout on each event; it now measures only when the page scrolls or resizes.

The result

Measured on the same preview deployment, before and after:

Mobile beforeMobile after
Performance score7897
Largest Contentful Paint3.4 s2.3 s
Total Blocking Time420 ms50 ms
Speed Index4.8 s3.1 s

Desktop went from 99 to 100. Visually, nothing changed.

What I took from it

"Looks fast" is not a measurement. On my own machine the hero felt immediate. The problem only existed on the devices most visitors actually use.

Ask what must wait for JavaScript. Anything that animates in from hidden is invisible until its animation runs. If that animation lives in a script, your most important content is waiting on that script.

Use the simplest tool that does the job. GSAP is excellent at scroll-driven choreography. A one-shot entrance does not need it.

The remaining gap is the reveal itself: the name is still masked for about a second while it animates in. That is a deliberate trade — it is the signature of the design — and now it is a trade I made knowingly rather than one I did not know I was making.