When we rebuilt digitalpixelweb.com on Next.js in early 2026, one of our internal goals was 100 mobile and desktop PageSpeed across every page. We hit it. This is the actual playbook — what worked, what didn't, and the parts that don't transfer to client sites.
A note up front: PageSpeed is a lab metric. It's not the same as Core Web Vitals from real users (CrUX field data), which is what Google actually uses for ranking. But a 100/100 lab score is hard to hit accidentally — it forces you to get the architecture right, and that pays dividends in field data. We'll cover both.
What "100/100" actually means
Lighthouse 12 (current at writing) scores Performance from 0–100 based on six metrics, weighted:
- First Contentful Paint (10%) — when the first text or image paints
- Largest Contentful Paint (25%) — when the largest hero element finishes rendering
- Total Blocking Time (30%) — sum of all main-thread blocking after FCP, lab proxy for INP
- Cumulative Layout Shift (15%) — visual stability during load
- Speed Index (10%) — how quickly above-the-fold content visually completes
- Interaction to Next Paint (10%) — responsiveness to user input
To score 100, every metric needs to be in the top bucket. There's no compensating with one great metric for one bad one. That's the trick — and the discipline.
The architecture decisions that did the heavy lifting
Most of our score comes from architecture, not optimization tweaks. If we'd built on a different stack, no amount of post-hoc tuning would have gotten us there.
Static generation everywhere it makes sense
/, /about, /capabilities, /portfolio, /contact, every blog post — all statically generated at build time. No server work on the request path. The CDN edge serves pre-rendered HTML in single-digit milliseconds. Even our blog index, which lists every post, is regenerated only at build time and ISR-revalidated.
This single decision eliminates server response time as a factor. PageSpeed's TTFB measurement on these pages is consistently under 200ms, which buys you tolerance for everything downstream.
App Router with aggressive Server Components
Next.js App Router with default Server Components means most of our component tree never ships JavaScript to the browser. Our Header, Footer, BlogCard, ProjectCard, CapabilityCard — all server-rendered. The only client components are the ones that genuinely need interactivity: the contact form, the portfolio filter bar, the GA tracking wrapper.
Result: the JavaScript bundle for the home page is around 90KB compressed. For comparison, a typical WordPress site we audit ships 600KB–1.2MB of JavaScript. The 7-13× reduction shows up in TBT and INP.
Image optimization with next/image (mostly)
next/image does a lot of work for free: format conversion (AVIF/WebP), responsive sizing, lazy loading below the fold, eager loading with priority for above-the-fold. We use it for everything except the blog covers (which are pre-sized to 1200×675 and served as-is).
The hero image on the home page uses priority and a properly-sized source. The above-the-fold project thumbnails on the portfolio page use it too. Below-the-fold images all default to lazy.
Font loading with next/font
next/font self-hosts Google Fonts at build time. No external font request. No FOUT. No layout shift from late-arriving fonts. Subset the character set if you only need Latin. Set display: swap so a fallback renders immediately if the webfont is delayed.
Before we switched to next/font, our LCP regressed by 200–400ms intermittently from font-driven layout shift. After: no font-related CLS, ever.
The optimizations that mattered after architecture
With the architecture right, here's what actually moved scores from "high 90s" to "100":
Defer everything non-critical
Google Analytics, Cloudflare Insights, any third-party script — all loaded with next/script strategy afterInteractive or lazyOnload. Nothing in beforeInteractive unless it's truly required for first paint.
We initially had GA loading early — TBT included its evaluation. Moving to lazyOnload cut TBT by 80ms, enough to push us from 95 to 100 on a marginal day. The cost: GA misses a tiny fraction of bounced sessions. Worth it. (We've since moved to fully deferred GA loading via a custom loader.)
Preload the LCP image
Even with priority on the hero Image, we add an explicit <link rel="preload" as="image" href="..." imagesrcset="..." imagesizes="..." /> in the head. This shaves 50–100ms off LCP on slow connections by getting the image request out before the HTML parser would otherwise discover it.
Inline critical CSS for above-the-fold content
Next.js handles this for you in production — CSS for above-the-fold rendering is inlined into the HTML. We don't override this. We do keep our above-the-fold CSS small enough that the inlined chunk doesn't bloat the document. Tailwind's content-aware purging is doing the actual work here.
Eliminate layout shift sources
CLS is brutal — one bad component can tank the whole score. Our checklist:
- Every
<Image>has explicit width/height - Every webfont has a fallback that occupies the same metrics (we use Tailwind's
font-sanswhich falls back to a system stack) - Every dynamically-loaded section (lazy components) reserves space with a min-height before mount
- No sliding banners, no late-arriving cookie banners, no dynamic ad slots
The cookie banner deserves a callout. The default implementation (load JS on render, render banner on init) causes CLS as the banner pushes content down. Solution: render the banner placeholder server-side at the bottom of the page (position: fixed with reserved height), then enable interactivity client-side. Banner is in the DOM from the first paint and never shifts content.
The optimizations that didn't matter
We tried things that turned out to be a waste of time. Saving you the effort:
Switching to AVIF manually
next/image already serves AVIF/WebP automatically when the browser supports it. We tried pre-encoding our blog covers to AVIF and serving them statically. Saved 5–15KB per image. PageSpeed didn't move. The transparent format negotiation already worked.
Hand-tuning Tailwind purge
Tailwind 3+ with proper content config purges aggressively by default. We tried adding aggressive whitelist/safelist to squeeze more out. Result: zero score change, plus we accidentally broke a couple of utility classes that were generated dynamically. Trust the defaults.
Running PostHog or similar instead of GA
Replacing GA with a "lighter" analytics platform doesn't move PageSpeed if you load it deferred. PostHog's bundle is similar to GA's. The win is from strategy="lazyOnload", not from changing vendors.
Aggressive bundling tweaks
We spent half a day fiddling with webpack configuration to inline a small library that was being code-split. Saved 1KB. Didn't move the score. Next.js' default bundling is good enough — leave it alone unless you can see a specific bundle in the network tab that genuinely shouldn't be there.
What this looks like on a client site
Here's the honest part: 100/100 is much harder on a typical client site than on our own.
The reason is third-party requirements. Marketing teams want HotJar. Sales want Drift. Compliance wants OneTrust. Each adds 50–200ms of TBT. Three of them and you're capped at 90.
What's realistically achievable on a client site:
- Static marketing site, well-architected: 95–100 mobile, 100 desktop. Achievable with the playbook above.
- WordPress site with maintained theme, no chat or A/B testing: 75–85 mobile, 90–95 desktop. Plugin overhead and PHP rendering set the ceiling.
- WordPress with chat widget + analytics + A/B testing: 60–75 mobile. The third-party scripts dominate.
- Drupal site, well-architected: 80–90 mobile, 95–100 desktop. Drupal's PHP overhead is similar to WordPress, but its admin/build pipeline produces less bloat.
The right target is field data passing Core Web Vitals, not lab data hitting 100. We've shipped client sites that hit 78 mobile lab and pass CrUX in the green because the field-data weighting is different from the lab weighting (real users on fast hardware skew the distribution upward).
The one optimization that's universal
If you change one thing this week to improve PageSpeed on any site, defer your third-party scripts. Audit every <script> tag. Anything that isn't critical to first paint gets defer, async, or — better — loaded after requestIdleCallback.
We've done this audit on five client sites in the last six months. Average improvement: 12 mobile PageSpeed points, with no functional regressions. Total time per audit: 30 minutes.
If you want help running the audit on your site, get in touch. We share the report and the implementation plan in the same engagement, no surprises.

