diff --git a/apps/web/content/blog/animated-hero-core-web-vitals.mdx b/apps/web/content/blog/animated-hero-core-web-vitals.mdx new file mode 100644 index 0000000..b212e14 --- /dev/null +++ b/apps/web/content/blog/animated-hero-core-web-vitals.mdx @@ -0,0 +1,56 @@ +--- +title: "An Animated Hero Without the Core Web Vitals Hit" +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 +--- + +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. + +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. + +## Two budgets: paint and main thread + +It helps to think of an animated hero as spending from two separate budgets. + +- **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. + +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. + +## Animate on the compositor, not the main thread + +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. + +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: + +- **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. + +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. + +## Your LCP element cannot be an inline SVG + +This one cost us a day, so learn it for free: **Chrome does not count an inline `` as a Largest Contentful Paint candidate.** Text, ``, `