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 before | Mobile after | |
|---|---|---|
| Performance score | 78 | 97 |
| Largest Contentful Paint | 3.4 s | 2.3 s |
| Total Blocking Time | 420 ms | 50 ms |
| Speed Index | 4.8 s | 3.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.