"Fast" gets thrown around a lot in frontend work, but it rarely means what people think it means. A Lighthouse score of 100 with a layout that jumps around while it loads is not fast. A 1.2MB hero image that's technically "optimized" is not fast. Fast is a page that feels stable and responsive the moment someone lands on it, on whatever device they actually own — which, for most of the sites I build (a trade business, a wedding invite, a money app), is a mid-range phone on patchy 4G, not a developer's M-series laptop on fiber.
I've shipped enough Next.js frontends now — from client marketing sites to full SaaS products — to have a short list of things I check before calling anything done. None of it is exotic. Most of it is just discipline.
Server components are the default, not the escape hatch
The App Router flipped the mental model: everything is a Server Component unless you explicitly opt into "use client". I still see codebases that mark whole pages as client components because one button needs an onClick. That drags the entire component tree — and its JS — into the client bundle for no reason.
My rule of thumb: push "use client" as far down the tree as it will go. A page can be a server component that renders static content and data, with a single small client island for the interactive bit.
// bad: whole page becomes client JS
"use client";
export default function ProductPage({ product }) {
return (
<div>
<ProductHero product={product} />
<ProductDetails product={product} />
<AddToCartButton productId={product.id} />
</div>
);
}
// better: only the interactive leaf ships client JS
export default function ProductPage({ product }) {
return (
<div>
<ProductHero product={product} />
<ProductDetails product={product} />
<AddToCartButton productId={product.id} /> {/* "use client" inside this file only */}
</div>
);
}The difference shows up directly in the JS payload the browser has to parse and execute before the page feels interactive — which matters far more on a mid-range Android phone than it does on your dev machine.
Images are still the biggest lever
On almost every project I've profiled, images are the single largest chunk of the page weight — bigger than JS, bigger than CSS, bigger than fonts combined. next/image handles resizing and lazy-loading automatically, but it only helps if you use it correctly:
- Set an explicit width and height (or use
fillwith a sized parent) so the browser reserves space before the image loads — this alone kills most layout shift. - Mark your actual hero image as
priorityso it isn't lazy-loaded behind the fold logic; everything else should lazy-load by default. - Serve real photography at the size it's displayed at. A 4000px camera photo squeezed into a 400px card is wasted bytes on every single visit.
For the trade and design-studio sites I've built, this one change — correctly sized, correctly prioritized images — did more for perceived speed than any JS optimization.
Fonts: pick a strategy and commit to it
Google Fonts through next/fontself-hosts and inlines the font files at build time, which avoids the extra round-trip to Google's CDN and the flash of unstyled text that comes with it. The part people skip is setting a proper fallbackfont stack that closely matches the metrics of the real font, so the page doesn't visibly reflow when the web font swaps in.
import { JetBrains_Mono } from "next/font/google";
const mono = JetBrains_Mono({
subsets: ["latin"],
weight: ["400", "500", "600"],
variable: "--font-mono",
display: "swap",
});Two font weights loaded instead of six is a real, measurable difference on slow connections. Audit which weights you actually use in the design before wiring up the font loader — it's a five-minute check that avoids shipping bytes nobody sees.
The static export trade-off
Not every project needs a Node server. This portfolio itself runs as a fully static export — output: "export" in next.config.ts— which means every route is pre-rendered HTML at build time, served from a CDN with no server round-trip at all. For content that doesn't need per-request personalization (marketing sites, documentation, portfolios, most small business sites), this is close to the fastest thing you can ship.
The trade-off is you lose server-side rendering per request, middleware, and API routes on the same deploy — you either don't need them, or you run a separate small backend (which is exactly what I do with FastAPI or Laravel when a project needs real server logic).
Layout shift is a UX bug, not a metrics problem
Cumulative Layout Shift gets treated like a Lighthouse score to chase, but the actual harm is concrete: a user starts tapping a button and the page shifts under their thumb because an ad, a font, or an image loaded in late. The fixes are boring and effective:
- Reserve space for anything that loads asynchronously — images, embeds, ads — with explicit dimensions.
- Don't inject banners or cookie notices above existing content after the page has already painted.
- Test on a throttled connection, not just your office wifi. Chrome DevTools' "Slow 4G" preset catches more real bugs than any synthetic score.
What I actually check before shipping
In practice, this is the short list I run through on every frontend before calling it done:
- Are client components pushed to the smallest possible leaf nodes?
- Does every image have explicit dimensions and the right loading strategy?
- Is the font stack minimal, self-hosted, and does it have a matched fallback?
- Does the page look and feel right on a throttled connection on a real phone, not just my monitor?
None of this is clever. It's just the difference between a site that's fast in a Lighthouse report and one that's fast for the person actually using it — and that second thing is the only one that matters.