An 8.7-Second Lie: How One Animation Killed My Mobile Score
01Eight point seven seconds for text that was already there
The report said mobile Largest Contentful Paint was 8.7 seconds. I did not believe it. The page was a static file on a fast server, the headline was plain text in the HTML, there was no font swap and no hero image to blame. I had watched it appear instantly on my own phone.
That is the part that took me two days to understand. It did appear instantly. To me.
My LCP was not slow. It was hiding.
What the browser was actually measuring
The headline started at opacity zero and faded in after a delay. To a person that reads as a nice entrance. To the browser it reads as an element that is not painted yet, so the clock keeps running, and Largest Contentful Paint gets recorded at the moment the text finally becomes visible rather than the moment it arrived.
The server had done its job in a few hundred milliseconds. My stylesheet then sat on the result and refused to show it. The browser does not time when your content arrives. It times when your content becomes visible.
02Animate movement, never visibility
The fix is smaller than the bug deserved. Keep the entrance, drop the fade. Move the element with transform and leave opacity alone, and the text is painted at full strength on the first frame while still sliding into place.
.hero h1 {
opacity: 0;
animation: rise .8s ease
.6s backwards;
}
@keyframes rise {
from { opacity: 0;
transform: translateY(24px) }
to { opacity: 1;
transform: none }
}
.hero h1 {
/* no opacity, ever */
animation: rise .8s ease both;
}
@keyframes rise {
from { transform: translateY(24px) }
to { transform: none }
}
Why the fixed version is also smoother
The second version is not a compromise. Transform is handled on the compositor, so the animation runs without repainting anything, which is the same reason it does not disturb the paint timing. The broken version was asking the browser to withhold content and do more work at the same time.
If an animation decides whether text is visible, it has stopped being decoration and started being a performance budget.
The family of bugs this belongs to
Once you know the shape, you find it everywhere. A fade up on a hero. A scroll reveal that starts every section at zero opacity. A skeleton loader that covers real content that already shipped. A cookie banner that paints over the headline. Each of them takes content the server delivered on time and hides it from the only clock that counts.
The server was fast. My stylesheet was the slow part.
03Five rules I now apply before any animation ships
Vitals are not only a ranking input. A slow first paint costs you the machines as well, because a fetcher on a clock takes what it has when time runs out, which is the same failure described in the case for shipping the words in the document. The agents that will or will not wait for you are already named in your log.
The rules
Nothing above the fold starts invisible. Not the headline, not the subhead, not the first paragraph. If it is the biggest thing on the screen, it must be painted the moment it arrives.
Entrances move, they do not appear. Transform for the motion, no opacity in the keyframes for anything in the first viewport. Below the fold you can fade whatever you like.
Delays are the multiplier. A fade is survivable. A fade that waits six hundred milliseconds first is how a fast server produces an eight second number.
Test on the throttled device, not yours. This bug is invisible on a fast phone on office wifi, which is exactly the machine every developer checks it on.
Re-measure after every redesign. Nobody reintroduces this on purpose. It comes back when a component gets restyled by someone who never saw the original number.
What it cost and what it taught
Two days of looking at the server, the font loading and the image pipeline, none of which were the problem, because I was searching for something slow and the actual fault was something hidden. When a number is impossible, stop optimising and start asking what the number is really measuring.
The animation is still there. It still slides. Nobody who visits the page could tell you which version they are looking at, which is the most annoying part of the whole story and also the point. The visible result was never the thing that needed to change, only the property doing the moving.
Tomorrow: how a model decides which brands to name, from a year of watching it happen.
>Why is my LCP slow when my server is fast?+
>Do CSS animations affect Core Web Vitals?+
>How do I fix a slow LCP caused by an animation?+
Abd Shanti. "An 8.7-Second Lie: How One Animation Killed My Mobile Score" CITED, Entry 020, Aug 13 2026. unknown.ps/blog/the-8-second-lie/
