DEV Community

NOGUCHILin
NOGUCHILin

Posted on

I rebuilt a slow landing page in Astro and took Lighthouse from 44 to 99

TL;DR

  • Rebuilding a heavy landing page in Astro moved the Lighthouse performance score from 44 → 99 (measured).
  • Most of the weight was JavaScript, images, and render-blocking resources. Astro's "zero JS by default" does the heavy lifting.
  • I ran the work async through Claude Code. The human only owns direction and verification.
  • There's a live before/after demo you can poke at (below).

This is the "make it fast" write-up. The flashy part (text exploding into particles and reassembling — glyphdust) gets one paragraph at the end.


Why Astro (what "slow" actually is)

When a landing page is slow, it's usually one of these:

  1. Too much JavaScript — decoration libraries, analytics tags, unused bundles.
  2. Unoptimized images — full-size PNGs, no width/height (hello CLS).
  3. Render-blocking — CSS/JS loaded synchronously in <head>.

Astro attacks this structurally:

  • Island architecture: zero client JS by default; only the components you mark with client:* get hydrated.
  • Image optimization: astro:assets <Image /> / <Picture /> handles width/height, lazy loading, and modern formats (the demo uses <Picture>).
  • Build-time rendering: HTML is static; CSS is easy to scope and inline.

The point isn't "switch frameworks." It's "structurally ship less JS."


What I actually did (checklist)

I used a fictional cafe ("KURADO") landing page, built twice with the same visual design: a Before (heavy) and an After (Astro).

[ ] Replace images with astro:assets <Picture /> (width/height set, modern formats auto)
[ ] Hero image loading="eager", everything else loading="lazy"
[ ] Self-host fonts; preload variable fonts with <link rel="preload" as="font" crossorigin>
[ ] Remove decorative JS, or island-ify it (client:visible, only when needed)
[ ] Scope CSS per component; drop the unused parts
[ ] async/defer third-party scripts; lazy-load where possible
Enter fullscreen mode Exit fullscreen mode

Note: for self-hosted fonts use preload, not preconnect (preconnect is for warming up connections to external origins). The After also preloads two latin-subset variable fonts.

Nothing exotic. Just killing JS, images, and blocking moves the numbers a lot.


Results (measured)

Same look, built two ways, Lighthouse on both (my own demo, fictional cafe):

Metric Before (heavy) After (Astro)
Performance score 44 99
LCP 13.9s 2.0s
TBT 1920ms 0ms
Transfer size baseline ~1/15
CLS ~0 ~0

Honest footnote: CLS was already ~0 in the Before (it wasn't a layout problem). Not inflating numbers matters, so I only claim the metrics that actually improved (LCP / TBT / transfer size).

LCP 13.9s → 2.0s, TBT 1920ms → 0ms. That's where the felt difference is.


Running it async with Claude Code

A human doesn't need to do all of this by hand. I hand it to Claude Code and keep direction and verification for myself.

  • "Replace this image set with astro:assets <Picture />, set width/height from intrinsic dimensions."
  • "Extract the XX decoration into a client:visible island."
  • "Run Lighthouse before/after and report the diff."

The key is verifying with an independent command — not "it should be faster," but actually running Lighthouse and reading the numbers. As long as the human holds that, the implementation can be safely delegated.


Signature: glyphdust

It's a speed post, but I leave one "signature" of my work on it: glyphdust, a small OSS react-three-fiber component I built.

Note: in the demo above, glyphdust is only named as a signature — the hero itself is a performance-first static implementation (piling on effects in a speed article would defeat the point). Here's what glyphdust does on its own.

With a single scroll progress 0 → 1, it shatters text into thousands of GPU particles → recomposes them into another glyph → and finally resolves to real, selectable DOM text.

import { GlyphDust } from "glyphdust";

<GlyphDust
  keyframes={[
    { type: "text", text: "KURADO", domSelector: "#headline" },
    { type: "scatter", spread: 1 },
    { type: "text", text: "FAST", dense: true, resolveToDom: true },
  ]}
/>
Enter fullscreen mode Exit fullscreen mode

three / @react-three/fiber / react are peer dependencies (React 18+, three 0.160+). With prefers-reduced-motion or no WebGL, it falls back to your own static markup (the screen never goes blank).

The last spoonful for pages that are both fast and memorable.


Demo, and if you want a rebuild

There's a live before/after demo you can touch (my own sample, fictional cafe):

If you've got a slow landing page or small site and want it fast without changing how it looks, I offer a fixed-scope, fully async (no calls) package on Contra:

Core Web Vitals pass guaranteed (if it doesn't pass, I keep working until it does). Delivery is a Loom walkthrough + a staging link. No meetings.


Takeaways

  • Astro "structurally reduces JS," so a slow LP's score can move a lot (44 → 99, measured).
  • The work is unglamorous (kill JS, images, blocking). You can run it async with Claude Code.
  • Always verify numbers by measuring. Don't inflate.
  • A spoonful of signature (glyphdust) for the finish.

Top comments (0)