Seven architectural decisions for Lighthouse 100
A clean 100 in production isn't a lucky bug fix — it's the sum of decisions made up front.
Lighthouse 100 isn't earned by polishing code at the end. It's earned by making the same seven decisions up front. We re-take them on every project — and that's why launch night isn't a panic for the last three points.
1. Server Components by default
If a page doesn't need interactivity, don't ship client JS. Marketing, blog, docs, product category pages — server only. 60–80% of bundle goes away with this one decision.
export default async function ServicePage({ params }) {
const { locale, slug } = await params;
const service = await getService(locale, slug);
return <ServiceTemplate service={service} />;
}
Rule of thumb
Pause two seconds before typing "use client". Is there real state — or just decoration?
2. Images: always next/image, always sized
Every img must arrive with explicit width and height. The single biggest LCP killer is CLS — and CLS comes mostly from sizeless images.
- Hero:
priority={true},fetchPriority="high" - Below-fold:
loading="lazy"is already default - AVIF + WebP fallbacks happen automatically
3. Fonts: subset + display swap
Latin-only subset, font-display: swap, self-hosted via next/font. A 200KB Inter file drops to 18KB when configured right.
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"], display: "swap" });
For Turkish content, use subsets: ["latin", "latin-ext"] — ş, ğ, ı live in latin-ext.
4. CSS: one file, critical inlined
With Tailwind v4 the bundle is itself a critical path concern. Atomic CSS lands at 5–15KB per route — Next 15 inlines anything ≤14KB.
5. Animation: GPU layer, single rail
Aurora blobs, parallax, micro-interactions — all on transform + opacity. Animating top or left doesn't show in Lighthouse but kills real-user FPS.
.aurora-blob {
transform: translate3d(0, 0, 0);
will-change: transform;
animation: aurora-shift 32s ease-in-out infinite;
}
Common mistake
backdrop-filter: blur(24px) on a sticky header halves scroll FPS. blur(12px) looks identical and ships.
6. Third-parties: always next/script strategy
Analytics, chat widgets, tag managers — all strategy="afterInteractive" or "lazyOnload". Nothing loads on the critical path. With Plausible you're at 1KB; with GTM, prefer direct GA4 + next/third-parties.
7. JSON-LD: build-time, not runtime
Generate schema markup in the server render — don't ship it to the client.
export default function Page() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(orgSchema),
}}
/>
<Hero />
</>
);
}
Seven decisions — and an eighth: measure. Run Lighthouse in CI on a mobile preset, set a performance budget. Production has a fixed target; a dip is a regression, not a polish task.