Skip to content

Repository files navigation

featherweight

A performance case study in the shape of a repo. It is one landing page for a fictional web performance monitoring product, built twice: an optimized version and a deliberately slow baseline. Both are deployed, and both are measured with Lighthouse against the live URL. The interesting part is not that the optimized page is fast, it is why each decision makes it fast, explained so it holds up in an interview.

The page ships in English and Portuguese. Each language is its own static route (/ and /pt/), not a client-side toggle, so the site keeps shipping zero JavaScript and a shared link keeps the language it was read in. The Lighthouse numbers below are measured against the English routes.

Built by Filipe Spanghero.


The case study

Same content, same layout, same design. The only thing that changes is how the page is delivered. The baseline commits the mistakes you see on real sites: fonts loaded from Google at runtime, a render blocking stylesheet, a blocking script in the head, and full size PNGs with no dimensions and no lazy loading. The optimized page fixes each one.

Numbers below come from Lighthouse 12 run against the live URLs. Mobile is the Lighthouse default (a throttled mid tier phone on slow 4G), which is where delivery mistakes actually hurt. You can reproduce them from Chrome DevTools or PageSpeed Insights.

Mobile (throttled, slow 4G)

Baseline Optimized
Performance 75 98
Accessibility 100 100
Best Practices 100 100
SEO 66¹ 100
Largest Contentful Paint 4.6 s 2.0 s
Cumulative Layout Shift 0 0
Total transfer 378 KB 129 KB

Baseline Lighthouse scores on mobile Optimized Lighthouse scores on mobile

Desktop

Baseline Optimized
Performance 95 99
Largest Contentful Paint 1.2 s 0.7 s
Total transfer 378 KB 135 KB

The extracted scores and metrics are committed in lighthouse-live/summary.json, so the numbers are not just claims in a table. Regenerate them any time with npm run build && npm run deploy followed by a Lighthouse run against the live URL.

¹ The baseline scores 66 on SEO because it is marked noindex. That is on purpose: it is a measurement artifact, not a page I want in search results. It is the one number where lower is correct.

Being honest about the numbers

  • Cumulative Layout Shift came out 0 on both pages. The optimized page sets explicit width/height on every image to reserve space, which is the right thing to do. On this particular layout the baseline did not shift enough to register either, so I am not going to claim a CLS win I did not measure. The win that did show up is Largest Contentful Paint and total bytes.
  • Optimized mobile is 98, not 100. The two missing points are First Contentful Paint and LCP under simulated slow 4G, where the hero image is bandwidth bound. The only Lighthouse opportunity left is about 85 ms of edge response time. There is no avoidable mistake to fix, so I would rather report 98 than tune the test until it says 100.

Technical decisions

Each of these is a lever the baseline pulls the wrong way.

Self hosted fonts instead of Google Fonts

The baseline links fonts.googleapis.com in the head. That is a render blocking request to a third party, a second connection to open, and a font that arrives after first paint (the flash of unstyled text).

The optimized page uses Astro's fonts API to download Space Grotesk and Inter at build time, subset them to the Latin range, and self host the woff2 files from the same origin. It preloads the two families used above the fold and lets Astro generate size adjusted fallback faces so the swap does not move layout. Result: no third party connection, no runtime dependency on Google, and the font is on the critical path from the first byte. This is also the more robust build: nothing is fetched from a network the deploy does not control.

Critical CSS inlined, no render blocking stylesheet

The baseline ships an external styles.css the browser must fetch and parse before it can paint. The optimized page inlines all of its CSS into the document (build.inlineStylesheets: 'always'), so the HTML arrives paint ready in a single request. The whole document is about 9 KB gzipped.

Modern image formats, sized and lazy

The three product mockups start as the same source PNGs. The baseline links them full size (up to 2560 px wide) with no width/height, no srcset, and no lazy loading. The optimized page runs them through Astro's <Picture>, which emits AVIF and WebP at several widths with sizes, sets explicit dimensions, marks the hero as fetchpriority="high", and lazy loads everything below the fold. The hero dashboard goes from a 112 KB PNG to a 9 KB AVIF at display size.

Zero JavaScript, and no blocking script

The baseline loads a script in the head with no defer, so the browser stops parsing to download and run it. The optimized page ships no JavaScript at all. The mobile menu is a native <details> element, the FAQ is a set of <details> accordions, and light and dark themes come from prefers-color-scheme. Nothing hydrates because there is nothing to hydrate.

Accessibility is not optional

Semantic landmarks, a skip link, labelled controls, alt text on every image, visible keyboard focus, and prefers-reduced-motion honored on the one animation (the latency marker). Accessibility scores 100 on both pages, and axe-core runs the full WCAG 2 A and AA rule set against both in CI.


Quality gates

The checks are automated so a regression fails the build rather than shipping.

Check Tool What it guards
Types astro check (strict) No any, no type errors
Performance and SEO budgets @lhci/cli Fails if Performance drops below 95 or Accessibility/SEO below 100, plus specific audits for modern image formats, responsive images, no render blocking resources, and font-display
Accessibility @axe-core/playwright Zero WCAG 2 A/AA violations on both pages
Smoke Playwright Page renders, hero image decodes, no failed requests, no render blocking script or external CSS, fonts are same origin

All of it runs on every push and pull request through GitHub Actions.

npm run typecheck   # astro check
npm run build       # static build to dist/
npm run test:a11y   # axe + smoke via Playwright
npm run lhci        # Lighthouse budgets against the built site

The Lighthouse config blocks *kaspersky-labs.com*, because some dev machines run an antivirus that injects its own scripts into local HTTP pages. That is not the page's code, so it is filtered out to measure what actually ships. In CI it is a no op.


How it is built

  • Astro 7, static output. No adapter, no server, no client JavaScript.
  • Fonts self hosted via the built in fonts API (fontProviders.google()).
  • Images optimized by Astro's Sharp service into AVIF and WebP.
  • Product mockups are generated from SVG by scripts/gen-assets.mjs and rasterized with Sharp, so the same source pixels feed both the optimized and baseline pages. The comparison measures delivery, not different artwork.
  • Deploy to Cloudflare Workers static assets with wrangler.
src/
  components/    UI, one concern each (Hero, Features, Showcase, Faq, ...)
  data/site.ts   all page copy, typed and shared by both pages
  layouts/       the document shell, meta and font preloads
  pages/
    index.astro          the optimized landing
    baseline/index.astro the deliberately slow version
  styles/global.css      design tokens and base styles
public/baseline/         the baseline's external CSS, blocking JS and raw images
scripts/                 asset generation and screenshot helpers

Run it locally

npm install
npm run assets   # regenerate the product mockups (optional, they are committed)
npm run dev      # localhost:4321

Deploy

npm run deploy   # astro build && wrangler deploy

License

MIT. See LICENSE.

Part of a small set of public projects. See also cable-ui (React component library) and quire (headless CMS), and the rest at filipe.span.dev.br.

About

A performance case study: one landing page built twice, baseline vs optimized, measured with real Lighthouse numbers. Astro, zero JS, Cloudflare.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages