Home / Blog / Critical CSS extraction: how it actually works in production

Critical CSS extraction: how it actually works in production

Inlining above-the-fold CSS shaves hundreds of milliseconds off FCP. Critical CSS extraction tools, build pipeline integration, and how to handle dynamic pages.

Critical CSS: the CSS needed to render the first fold of the page. Inline in <head> instead of as an external stylesheet, otherwise the browser shows a white screen until the stylesheet downloads.

In theory it’s a 100ms FCP win. In practice I see 300 to 600ms. Implementation is more delicate than people think.

Where the problem comes from

Modern sites ship 100 to 500 KB of CSS. The browser parses the HTML, hits <link rel="stylesheet">, fetches the CSS, and everything after that blocks on the fetch. While it fetches, the page is blank.

On 3G or mobile, fetching the CSS easily takes 500ms to 1.5s. That’s a full second of extra FCP.

Critical CSS solves it: inline the CSS needed for above-the-fold, load the main stylesheet async.

The basic pattern

<head>
    <style>
        /* inline critical CSS: 5 to 15 KB */
        body{margin:0;font-family:system-ui,...}
        .header{...}
        .hero{...}
    </style>
    <link rel="preload" href="/main.css" as="style" onload="this.rel='stylesheet'">
    <noscript><link rel="stylesheet" href="/main.css"></noscript>
</head>

Critical CSS is parsed immediately, enough for first paint. Main CSS loads async and is ready by the time the user scrolls.

How to extract critical CSS

Two approaches:

1. Manual. The designer or developer picks the above-fold components and keeps their CSS in an isolated file. Clean, but manual maintenance.

2. Automated extraction. A tool renders the page in a headless browser, detects which CSS rules are used inside the viewport, and extracts them.

Tools:

  • critical (npm package, from Addy Osmani): popular, built on penthouse
  • Penthouse: runs on Chrome headless
  • CriticalCSS API: cloud-based service
  • Critters (Google): webpack plugin, dev-time

I prefer the critical package. Node.js-based, with a CLI and a programmatic API.

How critical (npm) works

const critical = require('critical');

await critical.generate({
    src: 'dist/index.html',
    target: 'dist/index-critical.html',
    css: ['dist/main.css'],
    width: 1300,
    height: 900,
    inline: true
});

What the tool does:

  1. Renders the HTML in headless Chrome
  2. Sets the viewport (1300×900)
  3. Detects which CSS selectors the rendered page uses
  4. Extracts those rules from the main CSS
  5. Inlines them in the HTML and switches the main CSS to async

Ideal viewport sizes:

  • Desktop: 1300×900
  • Mobile: 375×667 (iPhone SE) or 390×844 (iPhone 14)
  • Tablet: 768×1024

You can generate separate critical CSS for each viewport, but in practice mobile plus desktop is enough.

Build pipeline integration

Critical CSS is generated at build time. Easy for static sites or build-based frameworks:

"scripts": {
    "build": "webpack && critical-css-extract"
}

Dynamic sites (WordPress, PHP) are trickier. Three approaches:

A. Per-template critical CSS. Generate critical CSS for homepage, single post, archive separately, save at build time, inline during template render.

In WordPress:

if (is_front_page()) {
    echo '<style>' . file_get_contents(get_template_directory() . '/critical-home.css') . '</style>';
} elseif (is_single()) {
    echo '<style>' . file_get_contents(get_template_directory() . '/critical-single.css') . '</style>';
}

B. Per-URL generation. A CDN or build service generates critical CSS for every URL. Cloudflare APO, Netlify build plugin, etc. Costly but automatic.

C. Runtime generation. Generating at request time (hacky, slow). Usually not recommended.

I prefer A. Pragmatic, controllable, maintainable.

The dynamic content problem

Generating critical CSS for dynamic pages is hard. The same homepage can look different to different users (A/B test, personalization, logged-in state).

Solution: generate a user-agnostic “shell” CSS. The base styles for header, navigation, hero structure, footer. The personalization layer loads async.

Critical CSS equals structure. Dynamic content equals regular CSS.

Size limit

How big should critical CSS be? The practical upper bound is ~14 KB.

Why 14 KB: the first TCP packet typically carries about 14 KB of data. HTML plus critical CSS that fits in that budget arrives in a single round trip, with no extra RTT.

Critical CSS over 15 KB can hurt more than it helps. Too large means you’ve got render blocking above the fold.

Modern HTTP/3 plus QUIC loosens the 14 KB limit a bit, but the rule of thumb still holds.

Measurement

To measure the impact of critical CSS:

Lighthouse. FCP is directly affected. Measure before and after.

WebPageTest. The film strip view shows visually “when did the page become visible”.

Real-User Monitoring. CrUX or the web-vitals library for real-user FCP metrics.

On one project, adding critical CSS:

  • FCP p75: 1.8s -> 1.1s (-39%)
  • LCP p75: 2.4s -> 1.9s (-21%)
  • CLS: unchanged

Potential pitfalls

1. Above-fold calculated wrong. Wrong viewport size means critical CSS is missing rules, and the first paint shows FOUC (flash of unstyled content).

2. Custom fonts. If critical CSS doesn’t include the font-face, the font loads async and you get FOIT (flash of invisible text) or FOUT (flash of unstyled text). You need font-display: swap plus preload.

3. JavaScript-dependent styling. If styles are applied to elements added by JS, the critical CSS tool can’t see them. Manual maintenance.

4. Dark mode and theme switching. Dynamic themes driven by prefers-color-scheme need to be in critical CSS, otherwise the user sees a split second of the wrong theme.

5. Regeneration discipline. Site design changes, a new component ships, the old critical CSS goes stale. If you don’t regenerate on every deploy, FOUC comes back.

CI pipeline integration

Wire critical CSS into your CI pipeline:

- name: Build
  run: npm run build

- name: Extract Critical CSS
  run: node scripts/extract-critical.js

- name: Test FCP
  run: lighthouse-ci --assert performance

Every deploy ships with fresh critical CSS, and regressions get caught in the alarm.

Alternative: server push or early hints

HTTP/2 server push (now deprecated) and HTTP/3 early hints (103 Early Hints) can be used to announce CSS preloads earlier.

Not as dramatic an improvement as critical CSS, but easier to implement. For small sites, 103 Early Hints might be enough, and critical CSS might be unnecessary.

On big sites, critical CSS plus early hints together works well.

Closing advice

Critical CSS is one of the middle-complexity techniques that seriously improves FCP. It can be automated, and the per-template generation pattern applies to dynamic sites too.

When auditing performance, if FCP is high, this is the first place to look. For FCP above 1.5 seconds, critical CSS extraction makes a big difference. Under 800ms, prioritize other optimizations.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch