X Xerobit

Critical CSS Inlining — Eliminate Render-Blocking CSS for Faster Load

Critical CSS inlining embeds above-the-fold styles directly in HTML to eliminate render-blocking requests. Learn how to extract critical CSS with tools like critical,...

Mian Ali Khalid · · 5 min read
Use the tool
HTML / CSS / JS Minifier
Minify HTML, CSS, or JavaScript. Strips whitespace, comments, and unnecessary characters. Shows size reduction percentage.
Open HTML / CSS / JS Minifier →

Render-blocking CSS delays the first paint: the browser must download and parse your stylesheet before drawing anything. Inlining critical CSS (above-the-fold styles) unblocks rendering and improves LCP.

Minify your HTML and CSS with the HTML Minifier.

What is critical CSS?

Without critical CSS inlining:

1. Browser receives HTML
2. Sees <link rel="stylesheet" href="styles.css">
3. BLOCKS rendering while downloading styles.css (100-500ms on 3G)
4. Parses CSS
5. Finally renders the page

With critical CSS inlining:

1. Browser receives HTML with <style>/* above-fold styles */</style>
2. Renders immediately (no blocking request needed)
3. Fetches full styles.css asynchronously in background
4. Loads non-critical styles without blocking

Manual approach: identify and inline critical styles

<!DOCTYPE html>
<html>
<head>
  <!-- Critical CSS inlined in <style> -->
  <style>
    /* Only styles needed for above-the-fold content */
    body { margin: 0; font-family: sans-serif; }
    header { background: #1e293b; color: white; padding: 1rem; }
    .hero { min-height: 60vh; display: flex; align-items: center; }
    .hero h1 { font-size: 2.5rem; margin: 0; }
    .nav { display: flex; gap: 1rem; list-style: none; }
  </style>
  
  <!-- Load full CSS non-blocking (preload trick): -->
  <link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
  <noscript><link rel="stylesheet" href="/styles.css"></noscript>
</head>
<body>
  <!-- ... page content ... -->
</body>
</html>

Automated extraction: critical npm package

npm install -D critical
// generate-critical-css.mjs
import critical from 'critical';

await critical.generate({
  inline: true,                    // Inline critical CSS into HTML
  base: 'dist/',
  src: 'index.html',
  target: 'index.html',            // Overwrite with inlined version
  dimensions: [
    { width: 375,  height: 812 },  // Mobile (iPhone SE)
    { width: 768,  height: 1024 }, // Tablet
    { width: 1440, height: 900 },  // Desktop
  ],
  penthouse: {
    timeout: 60000,
  },
});

console.log('Critical CSS generated and inlined');

The critical package:

  1. Opens the page in a headless browser at each viewport
  2. Identifies which CSS rules apply to visible content
  3. Inlines those rules in <style> tags
  4. Converts the <link rel="stylesheet"> to lazy-load

Critters (Google’s tool, lighter than critical)

npm install -D critters
// Webpack: critters-webpack-plugin
import Critters from 'critters-webpack-plugin';

export default {
  plugins: [
    new Critters({
      preload: 'swap',          // Use font-display: swap for fonts
      pruneSource: true,         // Remove inlined styles from main CSS
      logLevel: 'warn',
    }),
  ],
};
// Vite: vite-plugin-critical
import { critical } from 'vite-plugin-critical';

export default {
  plugins: [
    critical({
      criticalUrl: 'http://localhost:4173',
      criticalBase: 'dist',
      criticalPages: [
        { uri: '/', template: 'index' },
        { uri: '/about', template: 'about' },
      ],
      criticalConfig: {
        dimensions: [{ width: 1300, height: 900 }],
      },
    }),
  ],
};

Lazy-load remaining CSS without JavaScript

<!-- The preload + onload trick (works without JS for print media): -->
<link rel="stylesheet" media="print" onload="this.media='all'" href="/styles.css">
<noscript>
  <link rel="stylesheet" href="/styles.css">
</noscript>
<!-- Or with rel="preload" as="style": -->
<link rel="preload" href="/styles.css" as="style">
<link rel="stylesheet" href="/styles.css" media="(prefers-color-scheme: dark)">
<!-- Actually: just use the preload trick above, it's simpler -->

How much CSS to inline

Too little: missing styles cause layout shift (CLS)
Too much: inline CSS > ~14KB defeats the purpose
           (browsers can't cache inline styles)

Rule of thumb:
- 2-5KB of inline CSS is ideal
- Max ~10KB before inlining costs more than it saves
- Only styles needed for the first viewport (hero, header, nav)
// Measure critical CSS size:
const fs = require('fs');
const criticalCss = fs.readFileSync('dist/critical.css', 'utf8');
const gzipped = require('zlib').gzipSync(criticalCss).length;
console.log(`Critical CSS: ${criticalCss.length} bytes, gzipped: ${gzipped} bytes`);
// Aim for: < 5KB uncompressed, < 2KB gzipped

Combined with HTML minification

<!-- Before: -->
<style>
  /* lots of whitespace */
  body {
    margin: 0;
    padding: 0;
  }
</style>

<!-- After HTML minification: -->
<style>body{margin:0;padding:0}</style>

Use the HTML Minifier to minify the inlined style block along with the rest of the HTML.

Express: serve pre-generated critical CSS HTML

import express from 'express';
import { readFileSync } from 'fs';

const app = express();

// Serve pre-generated HTML with critical CSS inlined:
const indexHTML = readFileSync('dist/index.html', 'utf-8');

app.get('/', (req, res) => {
  res.set('Content-Type', 'text/html');
  res.set('Cache-Control', 'no-cache');  // HTML always fresh
  res.send(indexHTML);
});

Related posts

Related tool

HTML / CSS / JS Minifier

Minify HTML, CSS, or JavaScript. Strips whitespace, comments, and unnecessary characters. Shows size reduction percentage.

Written by Mian Ali Khalid. Part of the Frontend & Design pillar.