A Lighthouse 100 on desktop with Next.js 16: the levers that moved the score
This site (Next.js 16, App Router, every page statically prerendered) reached Lighthouse 100 on desktop for performance, accessibility, best practices, and SEO at once. The levers that got it there are concrete and few: all CSS inlined into the HTML, above-the-fold critical CSS carrying both themes, a server-rendered header with zero navigation hydration, and lazily hydrated islands with fixed-size placeholders. Mobile honestly sits at 77 to 84, and that is React hydration, not a broken config.
Which levers actually moved the score?#
| Lever | What it removes |
|---|---|
experimental.inlineCss | The render-blocking stylesheet request |
| Critical CSS with both themes | First-paint layout shift and theme flash |
| Server-only header | Navigation from the hydration cost |
| Lazy demo island, fixed placeholder | Below-fold JavaScript and CLS |
font-display: optional + idle work | Font swap jank, main-thread contention |
| Shiki at build time | A runtime highlighter |
All CSS in the document. With experimental.inlineCss, Next puts the whole stylesheet into every HTML page instead of linking it. For a static site with a stylesheet small enough to fit in the first responses, an extra round trip is a pure loss: the inlined CSS wins before any optimization of the CSS itself. This is the single biggest desktop lever, and it costs one config flag.
Critical CSS that carries both themes. The theme here is a data-theme attribute over CSS custom properties, dark by default with a light variant. The subtle trap: the inlined critical block must define both themes. Ship only the dark variables and the first paint after switching to light is a broken cascade, the kind of bug that shows up as a screenshot from a user, not in your own test run. One block, both variable sets, placed inline in the head.
A header that hydrates nothing. The header is a server component: logo, navigation, locale switch, and the theme toggle are plain HTML. The interactive bits (theme toggle, mobile menu as a native details element, the command palette trigger) are small client islands that hydrate independently, and the palette button works through a delegated listener rather than a handler in the markup. Navigation, the component every single page renders, costs zero JavaScript.
Below-fold islands with a fixed placeholder. The interactive Telegram shop demo on the landing page hydrates through an IntersectionObserver with rootMargin: 0, so nothing downloads until it is actually scrolled to. Its placeholder reserves the exact final size (379 by 509 pixels), which keeps CLS at zero even on the slowest connection. Lazy without a size reservation just trades jank for layout shift.
Fonts and idle work. font-display: optional removes swap jank: if the font is not there in time, the fallback renders and nobody watches text jump. Non-critical work waits for requestIdleCallback. Syntax highlighting happens at build time with Shiki, so no highlighter ships to the browser.
Why does mobile stay below 100?#
Because everything above buys back network and main-thread costs that React hydration then spends again. Under a 4x CPU throttle the total blocking time lands around 500 to 650 milliseconds: the pages arrive complete, then React hydrates i18n, theming, the palette, and the reading progress on the main thread. Lighthouse sees that and scores 77 to 84 on mobile, with everything except performance already at 100.
The honest options at that point: a static export with islands-only interactivity, or a framework whose rendering model starts from the server (the comparison in Livewire vs a React SPA is the same decision at application scale). Both are architecture changes, not config. I measured them against what the site is for and kept the React version: mobile 77 to 84 with instant navigation on real devices beats a gated 100.
Run Lighthouse on the production build, not next dev. The dev server injects overhead that can cost ten points and sends you optimizing things that are already fine. Every number in this article is from next build plus next start.
What the score does not show#
A 100 is not a certificate. The levers above are worth shipping for the user-visible effects alone: no render-blocking round trip, no theme flash, navigation that works before JavaScript parses. The number is a side effect of those decisions. Chased in reverse, it produces exactly the kind of site that scores 100 and feels slow, so treat every lever as a user experience first and re-measure after.