After the obvious LCP wins (optimize images, defer JS), the next 100-200ms usually hides in how the browser handles CSS and fonts during initial render. Most teams either ignore this layer or over-engineer it. Here’s the pattern that earns the last 200ms without adding maintenance pain.
Why CSS blocks rendering
Browsers won’t paint until they have the CSS needed to lay out the visible content. Every external stylesheet linked in the head delays the first paint by however long that stylesheet takes to download + parse. Inline critical CSS in the head, async-load the rest, and your first paint can be hundreds of ms faster.
Critical CSS extraction
Critical CSS is the subset of your stylesheets needed to render above-the-fold content. Inline it in the document head. The rest of the CSS loads asynchronously and doesn’t block first paint.
Tools that extract it automatically: critical (Node), critters (Webpack plugin, default in some frameworks), beasties (more recent alternative). All work by rendering the page in a headless browser, identifying which CSS rules applied to visible elements, and outputting that subset.
Next.js and critical CSS
Next.js 14+ with Tailwind 4 handles this well by default — the CSS for the route is inlined into the HTML response for static routes. You don’t need a separate extraction step. For larger CSS-in-JS setups or for routes that pull in heavy component CSS, the manual extraction is still worth it.
Font loading: the underrated LCP lever
Web fonts are heavy (50-200KB per face). If your headline uses a custom font, the LCP element doesn’t render until the font has loaded — that’s often 300-600ms of unnecessary wait. The fixes:
- Limit to two font families maximum, two weights per family. Every additional weight is another file.
- Preload the font files in the head (`<link rel="preload" as="font">`).
- Use font-display: swap or font-display: optional in the @font-face rule. Swap renders fallback text immediately and swaps when the font arrives; optional uses fallback if the font isn’t cached.
- Use size-adjust and other font-metric overrides to make the fallback render with similar metrics to the custom font, eliminating layout shift on swap.
next/font: the easy mode
Next.js ships `next/font` which handles preloading, font-display, and size-adjust automatically. For Google Fonts:
// app/layout.tsx
import { Inter } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
display: "swap",
weight: ["400", "600", "700"],
variable: "--font-inter",
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={inter.variable}>
<body>{children}</body>
</html>
);
}next/font self-hosts the font files (no external request to Google’s CDN), preloads them automatically, and generates the size-adjust overrides for you. Bigger LCP win than you’d expect.
Custom fonts (not from Google)
next/font supports local files via `localFont()`. Pass the woff2 file, the same display/preload options. Same wins.
import localFont from "next/font/local";
const heading = localFont({
src: [
{ path: "./fonts/Heading-Regular.woff2", weight: "400", style: "normal" },
{ path: "./fonts/Heading-Bold.woff2", weight: "700", style: "normal" },
],
display: "swap",
variable: "--font-heading",
});Avoiding layout shift on font load
When a fallback font has different metrics than the custom font, the text reflows when the custom font arrives — Cumulative Layout Shift. CSS `size-adjust`, `ascent-override`, `descent-override`, `line-gap-override` let you tune the fallback to match the custom font’s metrics so the swap is invisible.
next/font computes the overrides automatically. For hand-rolled @font-face rules, use font-metric tools to compute the right overrides.
What to do with above-the-fold images
If your hero is an image, it’s usually the LCP element. Three things to set:
- `priority` on next/image — preloads the source.
- Correct `sizes` attribute so the right resolution loads.
- AVIF/WebP format — next/image handles automatically.
What to do with above-the-fold text
If your hero is text (a big headline + dek), the LCP element is the headline. The lever is font loading. Inline the font in the critical CSS path. Preload the font file. Use font-display: swap so the text appears before the font is fully loaded — visible immediately, restyled when ready.
The 100ms wins compound
- Self-host fonts (no third-party DNS / TCP): 50-150ms saved.
- Preload critical font files: 100-200ms saved.
- Inline critical CSS: 50-150ms saved.
- Async-load non-critical CSS: 50-100ms saved.
- Use size-adjust to eliminate CLS on font swap: zero LCP impact but CLS dramatically improves.
Add them up: an LCP that was 2.4s drops to 1.8s after a couple of evenings of focused work. The work isn’t glamorous but it’s the work that moves Core Web Vitals into ‘good’ on borderline sites.
Measuring
Open Chrome DevTools, Performance tab, record a page load. Look at the Timing track. The LCP marker shows when LCP fired. Trace backwards: what loaded just before LCP? That’s the critical resource. Optimize it.
When this isn’t enough
If your LCP is still 2.5s+ after these optimizations, the issue isn’t CSS or fonts — it’s probably the origin server, the image strategy, or render-blocking JavaScript. Audit those layers separately. The CSS/font work is the last 200ms, not the first second.
How this lands across FH client work
Every FH client site uses next/font with the right font-display, preload, and size-adjust setup. Critical CSS is handled by Next.js automatically for our static routes. Hero images use next/image with priority. The combination consistently delivers under-2s LCP on mobile across the client book. If your site’s LCP is borderline and you’re not sure where to find the last 200ms, book a consultation — the audit usually finds 3-4 specific wins.