Skip to content

Critical CSS and Font Loading: The Last 200ms of LCP

Once you’ve fixed images and JS, font and CSS strategy is the next LCP lever. Here’s the pattern.

John Cravey with EleviFounder4 min read

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:

  1. Limit to two font families maximum, two weights per family. Every additional weight is another file.
  2. Preload the font files in the head (`<link rel="preload" as="font">`).
  3. 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.
  4. 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.

Written by
John Cravey
Founder

Founder of Frontend Horizon. Writes most of the long-form work on the FH blog.

Newer post
Cloudflare WAF and Bot Management for SMB Sites: The Rules That Actually Work
Older post
AI Image Generation for Marketing Sites: What Works, What Trips the Slop Detector
Keep reading

More from the blog

Performance·4 min

Core Web Vitals 2026: The Metrics That Matter and the Targets That Hold

Three numbers. Hit them and you’re competing on content; miss them and you’re competing one hand tied.

Performance·4 min

Optimizing INP: The Five Patterns That Fix Interaction Latency

INP is the hardest of the three Core Web Vitals to hit. Five patterns cover most of what we ship to fix it.

Professional Services·2 min

Site Speed and Core Web Vitals for Professional Services Firms

Speed is not a nice-to-have. It is the first impression, the ranking factor, and the conversion lever, all at once.