Skip to main content
← Back to Blog

My Animated Splash Was Tanking Our Mobile PageSpeed Score. It Wasn't the Image.

An animated hero, a mobile score stuck in the 60s, and a day spent learning the fix had nothing to do with the image.

My Animated Splash Was Tanking Our Mobile PageSpeed Score. It Wasn't the Image.

Our homepage opens with a splash: the SUCH mark glowing over a Cherenkov-blue field, a water ripple when you click through, a cross-fade into the page. I like it. On Google's Lighthouse test it scored 98 out of 100 on desktop. On mobile it sat in the 60s, sometimes failing outright, and the number dragging it down was Largest Contentful Paint (LCP).

A "paint" is the browser drawing pixels to the screen. First paint is when anything shows up. The largest contentful paint is when the biggest piece of real content, usually a headline or an image, finishes drawing. Google weighs LCP heavily because it stands in for "the page looks ready."

I chased that number, got it wrong the obvious way first, and one change fixed it. The short version: the fix was sending most of the page as plain HTML instead of as code the browser has to run. The image tricks I tried first barely moved it.

The obvious suspect

On mobile the tagline is hidden, so the biggest thing on screen is the logo. So you would naively optimize the logo. Seems reasonable.

First gotcha worth knowing: Chrome will not let a drawing written straight into the page (an inline SVG, for scalable vector graphics) count as the largest paint. Plain text counts. An <img> tag counts, even pointing at an SVG file. An <svg>...</svg> written into the HTML does not. Our logo was the inline kind, and on mobile it was the only big thing on screen, so the test had nothing valid to measure and reported an error. Swapping it for an <img> tag made the error go away, which felt like progress.

The score barely moved. LCP was still four or five seconds, and it jumped between runs. That jumping is a clue: a number that swings by whole seconds is not held back by one slow thing you can point at.

The test that ruled out the image

If the logo is slow, the usual suspect is the network: it is a separate file the browser has to fetch, and on a slow connection that fetch waits behind the CSS and JavaScript. Easy to test. Inline the image into the HTML as text (a data URI), so there is no separate file and no request. If the download was the problem, the logo should now paint about when the rest of the page first does, near a second.

It did not. With the image inlined, LCP was still about five seconds. The download was never the bottleneck. Whatever held the logo back happened after the bytes were already in hand.

Two timelines. Before: first paint at 1.1s, a four-second gap, LCP at 5.2s. After: first paint 1.1s, LCP 1.4s, gap gone.
The logo's pixels were ready early, but the browser did not draw them as the largest paint until seconds later. That gap is the thing to fix.

Where the time actually went

The clue was in front of me, but I initially misread it. Total Blocking Time (TBT) was green: about 30 milliseconds, which I took to mean the main thread was idle. It does not mean that. TBT only counts the part of a task that runs past 50 milliseconds, so a task that runs for 40 counts as zero. The page ran a lot of those, each just under the bar, and they added up.

Here is where they came from, and it is the part that is easy to get wrong. The page was already server-rendered: the framework sends real HTML, logo included, which is why it looked fine and ranked fine. But the whole page was also a client component, so the browser downloaded all of its JavaScript and re-ran it to wire up clicks and state. That second pass has a name, hydration, and it was a swarm of small tasks holding the main thread while the logo, already sitting in the HTML, waited to be drawn.

Lighthouse splits LCP into loading the element and then drawing it. My logo loaded instantly, since it was inline, so almost all of the five seconds was drawing: the pixels were ready and the browser was too busy hydrating to put them on screen. The filmstrip agreed, the logo showed up about halfway through the load, not at the start.

In the end I did not hunt down the exact line holding the paint back. I did not need to. The image was not slow. The page was.

The fix

So this was never "turn on server-side rendering." That was already on. The trap is subtler, and easy to spring by accident: one "use client" at the top of the page, added for a single interactive piece (the splash), pulls the entire page under it into the client bundle. Everything below the mark becomes code that ships and re-runs in the browser, and it still emits correct HTML the whole time, so nothing looks broken until you test on a slow phone.

The fix is to shrink that boundary. Most of a homepage is not interactive: a headline, some paragraphs, a few cards. That can stay server-only, plain HTML with nothing to ship and nothing to wake up. So I split it: the hero, the copy, and the sections are server-rendered now and hydrate to nothing, only the genuinely interactive parts (splash, header, bottom nav, animated background) ship as one small island of JavaScript.

Before: one client component holding everything, all shipping as JavaScript, paints late. After: a small client island plus server-HTML content, paints early.
Before, the entire page was interactive JavaScript. After, only a small island is; the rest is plain HTML the browser can show immediately.

Mobile went from the 60s to 95. Desktop held at 98. It stopped jumping between runs, because the largest paint no longer waits on a pile of JavaScript to settle.

What I would actually tell you

  • If a page is slow to appear, look at how much of it is interactive JavaScript. In the Next.js App Router, one "use client" at the top makes the whole page client code; push that mark down to the smallest piece that actually needs it.
  • To tell whether you are waiting on a file or on JavaScript, inline the slow image in the HTML and measure again. If nothing improves, stop optimizing the file.
  • A "responds quickly" score is not a "draws quickly" score. A green Total Blocking Time can still hide seconds of small tasks. Read the breakdown of the slow number, not the one next to it.
  • An image drawn straight into the HTML (inline SVG) will not count as the largest paint. Use an <img> tag.
  • Animate looping effects with CSS, not JavaScript. The browser moves and fades things on a separate track without interrupting your code; a JavaScript animation does work on every frame. (The desktop half of this story: different number, same lesson.)
  • None of it needed a content delivery network (CDN). The standard advice for a slow page is "put a CDN in front of it." This was about when things draw and what has to run as code. A CDN solves a different problem.

Why I care about the boring version

Performance is part of accessibility, which is a core tenet of this studio. A hero that feels instant on my laptop can be unusable on a cheap phone over a weak connection, and that phone is somebody's only device. Getting it to appear fast there is part of the same work as getting it to run with a screen reader or at 400% zoom.

The splash survived. It still shimmers and dissolves into the page, and it skips the intro entirely for anyone whose device asks for less motion.

When something is slow to appear, find out whether you are waiting on a file or on your own code before you optimize the wrong one.