I run a speed test on the SiteLens score page before I ship any landing. The current numbers: LCP under 800ms on 3G, TBT of zero, CLS of zero, Lighthouse score in the mid-90s. Every vertical. Every time.
This didn't happen by accident. It's a deliberate choice about what we include and what we don't. Let me walk through the decisions.
The Constraints
Before I built the landing-generator, I set hard constraints:
- Under 15KB of gzipped HTML per page.
- All CSS inline (no external stylesheet request).
- Zero JavaScript frameworks. Vanilla JS only, and only where load-bearing (mobile nav toggle, that's about it).
- Web fonts okay, but only from Google Fonts, and only DM Sans / DM Serif Display / JetBrains Mono (the Dangercorn type stack).
- Images optional. If present, responsive srcset.
- LCP in under 1s on simulated 3G.
Every one of these except web fonts is non-negotiable. Web fonts are negotiable because they're a user-facing design choice Jess and I both want to keep.
The Generator
Each landing is generated from a spec.json file in scripts/specs/. The spec has the vertical's title, tagline, hero description, feature list, pricing tiers, and related-vertical links. A Python script renders the spec through a single Jinja2 template that emits the final HTML.
# scripts/build_landing.py (simplified)
import json
from pathlib import Path
from jinja2 import Template
template = Template(Path("templates/landing.html.j2").read_text())
for spec_file in Path("scripts/specs").glob("*.json"):
spec = json.loads(spec_file.read_text())
output_path = Path(f"net/{spec['slug']}.html")
output_path.write_text(template.render(**spec))
130+ verticals regenerate in under 2 seconds. Change the template, everything re-renders with the new design. No build step in the user's path — they just get the pre-rendered HTML.
Inline CSS, Inline Everything
Every landing has its full CSS inlined in a single <style> block. No external stylesheet. No <link rel="stylesheet"> request. The CSS is ~5KB after minification because we use CSS variables heavily and don't load any framework.
The argument against inline CSS is cache-ability — if you load 10 pages from the same site, an external CSS loads once and caches. Inline CSS re-downloads per page.
For our traffic pattern, that's the wrong trade. Most vertical landing visitors come from a search result, land on one page, bounce or convert. They don't visit 10 different verticals in one session. Optimizing for "single page load is as fast as possible" beats optimizing for "the 10th page loads 5KB lighter."
No JavaScript Framework
React is not on these pages. Neither is Vue, Alpine, or Svelte. The only JavaScript is ~60 lines inline: mobile hamburger menu toggle, scroll-reveal for cards as you scroll down, and a nav-background-darkening effect on scroll. All of it is vanilla DOM API.
The win isn't philosophical — it's practical. A React landing page means shipping 40-80KB of React bundle, parsing it, executing it, before any content shows. Mine has ~2KB of inline JS that runs after the page is painted. The LCP gap is enormous.
I've got nothing against React for apps. I've got something against React for landing pages that are fundamentally static content.
Images and the Hero
Most of our landings don't have a hero image. The hero is typography + layout + the Dangercorn grid background. Zero image bytes on most pages.
Pages that do have images (the occasional product screenshot) use WebP with a JPEG fallback, responsive srcset, and loading="lazy" everywhere except above-the-fold. Above-the-fold images preload. Below-the-fold lazy-load.
The Font Trade
Google Fonts is the one external resource we load. DM Sans + DM Serif Display + JetBrains Mono. With preconnect hints, the font load is parallel with the HTML parse and usually doesn't block LCP.
Self-hosting fonts would shave another ~50ms and remove an external dependency. We've decided not to because (a) Google Fonts has better global CDN coverage than our one-server hosting, (b) the fonts change rarely, and (c) maintaining font subsetting for 130+ pages is its own category of work we don't want.
The OpenGraph Image
Every landing has an OG image (the thing that shows up in Slack/Twitter/LinkedIn previews). We generate these programmatically with Playwright — render the landing page, screenshot the hero section, save as og-{slug}.png. Runs at build time. Cached.
This is more work than using a generic site-wide OG image, but it's dramatically better when the landing gets shared. A generic OG image looks lazy; a per-page one looks intentional.
What 13KB Buys You
Concretely: on a 3G connection, a 13KB HTML page transfers in under 600ms including TCP handshake. On broadband, under 100ms. LCP follows shortly after because there's no waiting for external assets or hydration.
On mobile, the improvement is larger than on desktop. Mobile browsers parse and paint smaller HTML much faster because of lower-end CPUs. A 200KB Next.js page that renders in 200ms on my desktop renders in 1,800ms on a budget Android. 13KB renders in 400ms on the same budget Android.
What You Give Up
No client-side interactivity beyond what 60 lines of vanilla JS can do. If a landing needed a calculator widget or a complex interactive demo, we'd have to add JS — and I'd reach for HTMX or Alpine, not React.
No A/B testing infrastructure in the landing itself. We can still test via server-side variants (render two different slug-a.html and slug-b.html, route randomly), but we can't do fancy in-browser experiments.
No third-party analytics on-page by default. We measure traffic from server logs. If we wanted Google Analytics or Plausible, it'd cost some bytes. We've opted out.
What This Enables
Running 130+ landings on a $5 VPS with nginx handling static files. Zero backend load per landing visit. SEO crawls complete in seconds — Googlebot sees every landing as one fast request, indexes it immediately. Sitemap generation is trivial (every file in net/ is a URL).
It also enables us to iterate on the design. Change the template, rebuild 130 pages in 2 seconds, push to git, deploy. No cache-warming, no incremental rebuilds, no framework-level restart.
The cheapest, fastest, simplest web page is a pre-rendered HTML file on an nginx-served disk. Every layer you add on top should justify itself. Most can't.
Related
The app template (different concerns, same philosophy). Deterministic ports. Flask + SQLite at scale. Examples in the wild: cheesemaking, shiftfill, meetingmind. Run a speed test on any of them.