From 33400c5879374e63e67ce0b064667cfc5a25e87c Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 6 Jun 2026 20:41:31 -0400 Subject: [PATCH] Publish perf blog post + Such Moon Launch iOS links - New post 'My Animated Splash Was Tanking Our Mobile PageSpeed Score' (draft:false): the LCP investigation, the data-URI diagnostic, and the real fix (shrink the use-client boundary / server-render the static parts). Two SVG diagrams. - Such Moon Launch now live on iOS: App Store link wired on the apps and product pages. --- .../blog/animated-hero-core-web-vitals.mdx | 130 ++++++++++++++---- .../lcp-timeline.svg | 41 ++++++ .../server-vs-client.svg | 33 +++++ apps/web/src/app/apps/page.tsx | 3 +- apps/web/src/components/store-buttons.tsx | 3 +- 5 files changed, 177 insertions(+), 33 deletions(-) create mode 100644 apps/web/public/images/blog/animated-hero-core-web-vitals/lcp-timeline.svg create mode 100644 apps/web/public/images/blog/animated-hero-core-web-vitals/server-vs-client.svg diff --git a/apps/web/content/blog/animated-hero-core-web-vitals.mdx b/apps/web/content/blog/animated-hero-core-web-vitals.mdx index b212e14..25ae35e 100644 --- a/apps/web/content/blog/animated-hero-core-web-vitals.mdx +++ b/apps/web/content/blog/animated-hero-core-web-vitals.mdx @@ -1,56 +1,128 @@ --- -title: "An Animated Hero Without the Core Web Vitals Hit" +title: "My Animated Splash Was Tanking Our Mobile PageSpeed Score. It Wasn't the Image." date: "2026-06-06" -excerpt: "How we keep a custom animated splash and still score in the 90s on Lighthouse: animate on the right thread, and give the browser a real LCP element." -draft: true +excerpt: "An animated hero, a mobile score stuck in the 60s, and a day spent learning the fix had nothing to do with the image." +draft: false --- -Our homepage opens with an animated splash: the SUCH mark over a Cherenkov-blue field, a water-ripple when you click through, and a cross-fade into the page. We like it. It is also exactly the kind of thing that quietly wrecks a Lighthouse score, because a rich animated hero pushes on two of the metrics Google cares about most. Here is how we kept the effect and the score, and the handful of rules that made the difference. +# My Animated Splash Was Tanking Our Mobile PageSpeed Score. It Wasn't the Image. -This matters beyond a vanity number. Performance is part of accessibility. A hero that is smooth on our laptops can be unusable on a budget Android phone over a weak connection, and "works for everyone" is the whole pitch. Core Web Vitals is a rough proxy for that experience, so we treat it as a real constraint, not a scoreboard. +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). -## Two budgets: paint and main thread +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." -It helps to think of an animated hero as spending from two separate budgets. +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. -- **Paint timing** (First and Largest Contentful Paint): how quickly the big stuff shows up. -- **Main-thread time** (Total Blocking Time, and Interaction to Next Paint): how much JavaScript the page has to chew through before it can respond to you. +## The obvious suspect -A fancy hero can blow either one. The trick is knowing which technique spends from which budget, and not spending where you do not have to. +On mobile the tagline is hidden, so the biggest thing on screen is the logo. So you would naively +optimize the logo. Seems reasonable. -## Animate on the compositor, not the main thread +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 `` tag +counts, even pointing at an SVG file. An `...` 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 `` tag made the error go away, which felt like +progress. -The single biggest win: an always-on JavaScript animation runs a callback on the main thread every single frame. Drive a few looping effects that way and you can rack up *seconds* of blocking time on a mid-range phone, which tanks TBT and makes the page feel stuck. +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. -CSS animations on `transform` and `opacity` are different. The browser hands them to the compositor and they cost the main thread almost nothing. So the rule we follow now: +## The test that ruled out the image -- **Idle, always-on motion** (a logo float, a pulsing glow, a breathing outline): plain CSS `@keyframes`. -- **One-shot, interaction-driven motion** (the click-to-enter water ripple): JavaScript is fine, because it runs once and then stops. +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. -Moving our splash's idle animations off the JS animation library and into CSS took the homepage's blocking time from "the page is janky for several seconds" to a rounding error, with no visible change to the effect. One caveat: not every CSS property is cheap. Animating something like `stroke-dashoffset` still forces a repaint every frame, so it is "CSS" but not free. Stick to `transform` and `opacity` for the things that loop forever. +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. -## Your LCP element cannot be an inline SVG +
+ 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.
+
-This one cost us a day, so learn it for free: **Chrome does not count an inline `` as a Largest Contentful Paint candidate.** Text, ``, `