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:
- Renders the HTML in headless Chrome
- Sets the viewport (1300×900)
- Detects which CSS selectors the rendered page uses
- Extracts those rules from the main CSS
- 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 performanceEvery 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.