A printing press stamping the top of a web page while a red Maine Coon cat watches from a shelf.

Critical: Painting the Fold Before the Stylesheet Lands

The Orange Cat
The Orange Cat

Every browser plays the same game before it can paint a single pixel: it has to build the render tree, and CSS is render-blocking. Drop a <link rel="stylesheet"> in your <head> and the browser will dutifully download and parse the entire stylesheet, rules for footers and modals and pages nobody has scrolled to yet, before it dares to show the first frame. On a fat CSS bundle that is a measurable delay to First Contentful Paint and Largest Contentful Paint, and a direct hit to your Core Web Vitals.

Critical is the long-running, battle-tested tool that fixes this. Created by Addy Osmani of the Google Chrome team and now maintained by Ben Zörb, the critical package figures out exactly which CSS rules are needed to render the visible, above-the-fold portion of a page at a given viewport, inlines just those rules into the HTML <head>, and arranges for the remaining "uncritical" CSS to load asynchronously. The result is a first paint that is already styled (no Flash of Unstyled Content) and arrives in a single round trip with nothing blocking it. It works on any HTML, which makes it a natural fit for static sites, SSG output, and server-rendered React or Vue pages.

Why It Earns Its Keep

  • Real-browser accuracy. Critical drives a headless browser through Penthouse to actually render your page, so it knows which rules apply to the fold even in genuinely complex layouts.
  • Inline or extract. Inline the critical CSS into your HTML, emit it as a standalone file, or do both, with the inlined rules optionally stripped from the original stylesheet so nothing ships twice.
  • Responsive by design. Generate critical CSS across multiple viewport dimensions in a single pass, so phones and desktops each get a fold that fits.
  • Surgical control. ignore rules, at-rules, or declarations by string, regex, or predicate function. Rebase asset URLs to a CDN. Inline small images as base64. Minify with clean-css.
  • Pipelines everywhere. A Node API (callbacks, Promises, or async/await), a CLI that reads stdin and file paths, and a native Gulp stream interface.

Getting It Onto Your Machine

Critical 8 is ESM-only and wants Node 22.13 or newer. Install it as a dev dependency:

npm install --save-dev critical
yarn add --dev critical

If you are on an older CommonJS-only project or stuck below Node 22.13, pin to an earlier major while you plan the migration to ESM.

Your First Inlined Fold

The core of the library is the generate function. Point it at an HTML file, give it a viewport, and ask it to inline the critical CSS into a new file:

import { generate } from "critical";

await generate({
  inline: true,
  base: "dist/",
  src: "index.html",
  target: "index-critical.html",
  width: 1300,
  height: 900,
});

Critical reads dist/index.html, discovers the stylesheets it references, renders the page at 1300x900, determines which rules touch the above-the-fold region, and writes dist/index-critical.html with those rules inlined in the <head>. The leftover CSS is rewired to load asynchronously, so the link no longer blocks the first paint. The base option is your anchor for resolving the source and any relative asset paths.

Holding the Result in Your Hands

You do not have to write to disk. Skip inline and target, and generate resolves to an object you can destructure, which is ideal when Critical is one step inside a larger build script:

const { css, html, uncritical } = await generate({
  base: "dist/",
  src: "index.html",
  width: 1300,
  height: 900,
});

console.log(`Critical CSS weighs ${Buffer.byteLength(css)} bytes`);

Here css is the extracted critical CSS, uncritical is everything that did not make the fold, and html is the transformed markup (populated when you ask for inlining). Want the critical CSS as a plain file instead of inlined HTML? Just give target a stylesheet path:

await generate({
  base: "dist/",
  src: "index.html",
  target: "styles/critical.css",
  width: 1300,
  height: 900,
});

One Fold Per Screen Size

A single 1300x900 viewport is a reasonable default, but real users arrive on phones, tablets, and ultrawides. The dimensions array lets Critical render the page at several sizes and union the rules each one needs, so the inlined block covers every fold you care about:

await generate({
  base: "dist/",
  src: "index.html",
  target: { css: "styles/critical.css" },
  dimensions: [
    { width: 500, height: 200 },
    { width: 1200, height: 900 },
  ],
});

When dimensions is present it overrides the single width/height pair. Notice that target is now an object: you can split outputs into css, html, and uncritical keys independently, which pairs nicely with the next trick.

Trimming, Rebasing, and the Full Split

Critical's deeper options are where it pulls ahead of lighter alternatives. The ignore option keeps specific rules out of the critical block, by exact at-rule name, by regex against a selector, or by a predicate that inspects each declaration. The rebase option rewrites asset URLs, handy when your built CSS references local paths but production serves from a CDN:

await generate({
  base: "dist/",
  src: "index.html",
  target: { css: "styles/critical.css" },
  ignore: {
    atrule: ["@font-face"],
    rule: [/\.tooltip/],
    decl: (node, value) => /hero-background\.png/.test(value),
  },
  rebase: (asset) => `https://cdn.example.com${asset.absolutePath}`,
});

Ignoring @font-face is a common move: fonts often load on their own swap schedule, and pulling their declarations into the critical block rarely helps the first paint. When you want every output broken out explicitly, combine extract with a full target object so the original stylesheet ships without the rules you already inlined:

await generate({
  inline: true,
  base: "dist/",
  src: "index.html",
  css: ["dist/styles/main.css"],
  extract: true,
  target: {
    css: "critical.css",
    html: "index-critical.html",
    uncritical: "uncritical.css",
  },
});

With extract: true, the rules that made it into critical.css are removed from uncritical.css, so visitors never download the same declarations twice. You can also flip on inlineImages to base64-embed images below maxImageFileSize (10240 bytes by default), shaving a few more requests off the critical path.

Wiring It Into a Build

Critical ships a native Gulp stream, so it slots into an existing asset pipeline with a single pipe:

import gulp from "gulp";
import { stream as critical } from "critical";

gulp.task("critical", () =>
  gulp
    .src("dist/*.html")
    .pipe(
      critical({
        base: "dist/",
        inline: true,
        css: ["dist/styles/components.css", "dist/styles/main.css"],
      })
    )
    .pipe(gulp.dest("dist"))
);

Prefer the command line? The CLI reads HTML from stdin or a path and writes to stdout, which composes well with the rest of your shell:

cat dist/index.html | critical --base dist --inline > dist/index.critical.html

For build systems beyond Gulp, the ecosystem offers wrappers like grunt-critical for Grunt and html-critical-webpack-plugin for Webpack, all driving the same engine underneath.

Things Worth Knowing Before You Ship

Critical is powerful precisely because it renders your page for real, and that real render is also its main cost. A few realities to keep in mind:

  • It is a build-time, static snapshot. Critical analyzes the HTML as it exists when you run it. For client-rendered React apps, point it at your rendered SSR or SSG output, not the empty <div id="root"> shell, otherwise it finds almost nothing above the fold.
  • Runtime-injected styles can slip through. CSS-in-JS that appends a <style> tag at runtime may not be present in the declared stylesheets Penthouse inspects, so it can be missed. Critical is happiest with statically declared CSS.
  • The headless browser is a dependency. Penthouse launches a real browser, which means runs are heavier and slower than DOM-less tools, and CI or Docker images may need Chromium dependencies installed.
  • Critical CSS goes stale. Because the inlined block is generated from a specific markup-and-CSS pairing, it drifts when you change one and forget to regenerate. Wire the generation step into your build so it always runs.

If you want a fast, zero-browser bundler plugin and your CSS is declared statically, a tool like beasties may serve you better. Reach for Critical when you want maximum accuracy from an honest browser render, need a CLI or Gulp pipeline, or want fine-grained control over multiple viewports, ignore rules, image inlining, and asset rebasing, especially against static or server-rendered HTML.

Closing the Loop

Render-blocking CSS is one of those problems that hides in plain sight: the page works, it just shows up a beat late. Critical attacks that beat directly. By rendering your page in a real browser, lifting out exactly the styles the first screen needs, inlining them, and deferring the rest, it gives you a first paint that is both fast and fully styled, no flash of unstyled content along the way. With responsive dimensions, ignore rules, asset rebasing, and a choice of Node API, CLI, or Gulp stream, it stays flexible enough to sit in almost any build. If your Core Web Vitals are quietly bleeding on that opening stylesheet request, Critical is a mature, well-worn tool worth pointing at your HTML.