|$ curl https://forge-ai.dev/api/markdown?path=docs/css/critical
$cat docs/critical-css.md
updated This Week·10 min read·published

Critical CSS

CSSCriticalPerformanceAdvanced
What is Critical CSS?

Critical CSS is the technique of identifying and inlining the minimum CSS required to render the above-the-fold content of a page. The remaining styles are loaded asynchronously after the initial render.

The goal is to eliminate render-blocking CSS requests. When a browser encounters a <link rel="stylesheet">, it pauses rendering until the stylesheet is downloaded and parsed. By inlining critical styles directly in the <head>, the page renders immediately, dramatically improving perceived performance and Core Web Vitals scores.

critical-css-html.html
HTML
1<!DOCTYPE html>
2<html>
3<head>
4 <!-- Critical CSS — inlined, renders immediately -->
5 <style>
6 body { font-family: monospace; margin: 0; background: #0D0D0D; color: #E0E0E0; }
7 .header { display: flex; padding: 16px; background: #1A1A2E; }
8 .hero { min-height: 60vh; display: flex; align-items: center; justify-content: center; }
9 .hero h1 { font-size: clamp(2rem, 5vw, 4rem); color: #00FF41; }
10 </style>
11
12 <!-- Non-critical CSS — loaded async -->
13 <link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
14 <noscript><link rel="stylesheet" href="styles.css"></noscript>
15</head>
16<body>
17 <header class="header">...</header>
18 <section class="hero">
19 <h1>Above the fold content</h1>
20 </section>
21</body>
22</html>
Above-the-Fold Rendering

"Above the fold" refers to the portion of the page visible without scrolling. The exact content varies by device — a 1920px desktop screen shows more above the fold than a 375px mobile phone. Critical CSS must account for multiple viewport sizes.

above-the-fold.css
CSS
1/* Critical CSS for mobile (375px) */
2@media (max-width: 767px) {
3 .hero { min-height: 100vh; padding: 16px; }
4 .hero h1 { font-size: 1.75rem; }
5 .nav { display: flex; justify-content: space-between; }
6}
7
8/* Critical CSS for tablet (768px) */
9@media (min-width: 768px) and (max-width: 1023px) {
10 .hero { min-height: 80vh; padding: 32px; }
11 .hero h1 { font-size: 2.5rem; }
12}
13
14/* Critical CSS for desktop (1024px+) */
15@media (min-width: 1024px) {
16 .hero { min-height: 60vh; padding: 64px; }
17 .hero h1 { font-size: 4rem; }
18}
19
20/* These are the only styles needed for initial render.
21 Everything else (footer, comments, sidebar, modals)
22 can be loaded asynchronously. */

best practice

Include critical styles for mobile, tablet, and desktop in your inlined CSS. A common mistake is only optimizing for one viewport — the Lighthouse score on other devices will suffer.
Extracting Critical CSS

Manual extraction does not scale for real projects. Automated tools analyze your rendered HTML against your CSS to determine which rules apply above the fold. The process typically involves:

Step 1: Load the page in a headless browser (Puppeteer/Playwright)
Step 2: Set viewport sizes for mobile, tablet, and desktop
Step 3: Extract all computed styles for visible elements only
Step 4: Deduplicate and minify the extracted CSS
Step 5: Inline the result in the HTML head
extract-critical.js
JavaScript
1// Critical CSS extraction with Puppeteer
2const puppeteer = require('puppeteer');
3const { critical } = require('critical');
4
5async function generateCriticalCSS(url) {
6 const result = await critical.generate({
7 base: '/path/to/site',
8 src: url,
9 targets: {
10 critical: 'critical.css',
11 stylesheet: 'styles.css', // remaining styles
12 },
13 width: [375, 768, 1280], // viewports to target
14 height: [667, 1024, 800],
15 inline: true, // inline critical CSS
16 minify: true,
17 });
18
19 return result;
20}
21
22// Programmatic usage
23critical.generate({
24 inline: true,
25 base: 'dist/',
26 src: 'index.html',
27 target: 'index-critical.html',
28 width: [375, 768, 1280],
29 height: [667, 1024, 800],
30});
Inline Critical CSS

Inlined CSS is embedded directly in the <style> tag within the <head>. This eliminates network latency for the initial render — the browser has the styles immediately.

inline-critical.html
HTML
1<head>
2 <!-- Inlined critical CSS — no network request needed -->
3 <style>
4 /* Critical styles for hero, nav, above-fold content */
5 *,*::before,*::after{box-sizing:border-box}
6 body{margin:0;font-family:system-ui;background:#0D0D0D;color:#E0E0E0}
7 .header{display:flex;align-items:center;padding:0 24px;height:64px;background:#1A1A2E}
8 .hero{min-height:70vh;display:flex;align-items:center;justify-content:center;flex-direction:column;padding:24px;text-align:center}
9 .hero h1{font-size:clamp(2rem,5vw,4rem);color:#00FF41;margin:0 0 16px}
10 @media(max-width:767px){.hero h1{font-size:1.75rem;}}
11 .hero p{font-size:clamp(0.875rem,2vw,1.25rem);color:#A0A0A0;max-width:600px}
12 </style>
13
14 <!-- Preload non-critical CSS, switch to stylesheet when loaded -->
15 <link rel="preload" href="/styles/main.css" as="style"
16 onload="this.onload=null;this.rel='stylesheet'">
17 <noscript><link rel="stylesheet" href="/styles/main.css"></noscript>
18</head>
📝

note

Keep inlined critical CSS under 14KB (compressed) to avoid exceeding the initial TCP congestion window. Above 14KB, the browser needs an extra round trip, negating some of the performance benefit.
Loading Non-Critical CSS Async

The remaining CSS should be loaded without blocking the initial render. Several techniques achieve this, each with different trade-offs.

Preload + onload

preload-onload.html
HTML
1<!-- Most common technique — preload, then swap -->
2<link rel="preload" href="styles.css" as="style"
3 onload="this.onload=null;this.rel='stylesheet'">
4<noscript><link rel="stylesheet" href="styles.css"></noscript>

Media Query Trick

media-query-trick.html
HTML
1<!-- Load with non-matching media query, then switch -->
2<link rel="stylesheet" href="styles.css" media="print"
3 onload="this.media='all'">
4<noscript><link rel="stylesheet" href="styles.css"></noscript>

JavaScript Load

async-load.js
JavaScript
1// Load CSS dynamically
2function loadCSS(href) {
3 const link = document.createElement('link');
4 link.rel = 'stylesheet';
5 link.href = href;
6 document.head.appendChild(link);
7}
8
9// Intersection Observer — load when below-fold content is near viewport
10const observer = new IntersectionObserver((entries) => {
11 entries.forEach(entry => {
12 if (entry.isIntersecting) {
13 loadCSS('/styles/comments.css');
14 observer.unobserve(entry.target);
15 }
16 });
17});
18
19observer.observe(document.querySelector('.comments-section'));
Tools

Several tools automate the critical CSS workflow. Each integrates differently with your build pipeline.

ToolTypeBest For
CriticalNode.js libraryFull control, build pipeline integration
PurgeCSSCSS optimizerRemoving unused CSS from production builds
PenthouseNode.js libraryPuppeteer-based extraction with viewport control
webpack-criticalWebpack pluginAutomatic integration with webpack builds
vite-plugin-criticalVite pluginVite-based projects
next-css-optimizeNext.js pluginNext.js applications

Critical CLI Usage

critical-cli.sh
Bash
1# Install
2npm install --save-dev critical
3
4# CLI usage
5npx critical --base dist --src index.html --target index.html --width 375 768 1280 --height 667 1024 800
6
7# Inline mode — modifies HTML in place
8npx critical --base dist --src about.html --target about.html --inline

PurgeCSS

purgecss.config.js
JavaScript
1// purgecss.config.js
2module.exports = {
3 content: ['./src/**/*.html', './src/**/*.jsx', './src/**/*.tsx'],
4 css: ['./src/**/*.css'],
5 safelist: {
6 standard: [/^theme-/, /^is-/], // keep dynamic classes
7 deep: [/modal$/],
8 },
9};
Performance Metrics — FCP & LCP

Critical CSS directly impacts two Core Web Vitals metrics: First Contentful Paint (FCP) and Largest Contentful Paint (LCP).

MetricWhat it MeasuresImpact of Critical CSS
FCPTime when first text or image appearsEliminates render-blocking CSS, content appears earlier
LCPTime when largest content element rendersHero images and headings style immediately, no flash
TBTTotal Blocking Time (main thread)Reduced CSS parsing overhead on the main thread
CLSCumulative Layout ShiftProper critical CSS prevents layout shifts from async CSS

info

A typical improvement from implementing critical CSS: FCP drops from 2.5s to 1.0s, and LCP from 4.0s to 1.8s on 3G connections. This alone can move a site from "needs improvement" to "good" on Core Web Vitals.
Code Splitting CSS

Critical CSS pairs naturally with CSS code splitting. Rather than one monolithic stylesheet, split CSS by route, component, or feature, and load each chunk only when needed.

css-splitting.css
CSS
1/* Route-based splitting */
2/* home.css — critical path */
3.hero { /* ... */ }
4.features { /* ... */ }
5
6/* dashboard.css — lazy loaded */
7.chart { /* ... */ }
8.data-table { /* ... */ }
9
10/* Component-based splitting */
11/* button.css */
12.btn { /* ... */ }
13.btn--primary { /* ... */ }
14
15/* modal.css — only loaded when modal opens */
16.modal-overlay { /* ... */ }
17.modal-content { /* ... */ }

Dynamic Import Pattern

dynamic-import.jsx
JavaScript
1// React — lazy load component CSS
2import { lazy, Suspense } from 'react';
3
4const Dashboard = lazy(() => import('./Dashboard'));
5
6// Dashboard.jsx — CSS imported with component
7import './Dashboard.css';
8
9// Vite — CSS code splitting automatic with dynamic imports
10const AdminPanel = lazy(() => import('./AdminPanel'));
11
12// Next.js — CSS Modules scoped per page
13// Each page.tsx imports its own .module.css
14// Next.js automatically extracts critical CSS per route
Example Workflow

A production-grade critical CSS workflow integrates extraction, inlining, and fallback loading into your build pipeline.

build-workflow.js
JavaScript
1// build.js — example build pipeline
2const { build } = require('vite');
3const critical = require('critical');
4const { PurgeCSS } = require('purgecss');
5
6async function buildWithCriticalCSS() {
7 // 1. Build the application
8 await build();
9
10 // 2. Purge unused CSS
11 const purged = await new PurgeCSS().purge({
12 content: ['dist/**/*.html'],
13 css: ['dist/**/*.css'],
14 });
15
16 // 3. Generate critical CSS for each page
17 const pages = ['index', 'about', 'docs', 'pricing'];
18
19 for (const page of pages) {
20 await critical.generate({
21 base: 'dist',
22 src: `${page}.html`,
23 target: `${page}.html`,
24 inline: true,
25 width: [375, 768, 1280],
26 height: [667, 1024, 800],
27 minify: true,
28 });
29 }
30
31 // 4. Result: each HTML file has inlined critical CSS
32 // with async-loaded full stylesheet
33 console.log('Critical CSS generated for all pages');
34}
35
36buildWithCriticalCSS();

Verifying the Result

verify-critical.sh
Bash
1# Check if CSS is render-blocking
2curl -s https://your-site.com | grep -o '<link[^>]*stylesheet[^>]*>'
3
4# Verify critical CSS is inlined
5curl -s https://your-site.com | grep -o '<style>.*</style>' | head -c 100
6
7# Lighthouse CI check
8npx lighthouse https://your-site.com --quiet --output=json | jq '.categories.performance.score'
9
10# WebPageTest
11# https://www.webpagetest.org/ — check "render-blocking resources"
Best Practices
Keep inlined critical CSS under 14KB gzipped to fit in one TCP round trip
Generate critical CSS for multiple viewports (mobile, tablet, desktop)
Automate extraction in your build pipeline — manual maintenance does not scale
Use rel=preload with onload swap for loading non-critical CSS
Always include a <noscript> fallback for browsers without JavaScript
Pair critical CSS with CSS code splitting for maximum performance
Monitor FCP and LCP in production to validate your critical CSS strategy
Purge unused CSS before generating critical CSS to keep extraction small
Test with real 3G throttling — not just dev tools fast refresh
$Blueprint — Engineering Documentation·Section ID: CSS-35·Revision: 1.0