Critical 9: how to extract critical CSS without a browser (and when you actually need it)

Critical 9, published on September 13, 2026, extracts critical CSS without a browser: on pages that already have their HTML rendered it calculates the CSS above the fold in milliseconds, and only opens Chromium (via Playwright) when the HTML is an empty SPA shell. It’s a complete rewrite of Addy Osmani’s tool (~10.3k stars on GitHub), and the practical result is clear: the common case—static output, SSR, or MPA—no longer needs a headless browser in your build.

What is critical CSS?

Critical CSS is the subset of styles needed to paint the first screen, that is, the content above the fold. By default, every <link rel="stylesheet"> in the <head> is render-blocking CSS: the browser paints nothing until it downloads and processes it. Tools like Critical extract the rules that style the visible content, insert them in a <style> at the beginning of the <head>, and load the rest of the stylesheet without blocking the first paint.

How does critical CSS improve LCP and First Contentful Paint?

By removing stylesheets from the critical path, the browser can paint the first screen with just the HTML. Critical’s README presents it as one of the most direct levers on the Largest Contentful Paint (LCP), and the release notes talk about reaching the first paint sooner (First Contentful Paint).

What’s new in Critical 9?

The main change: Critical now includes two engines and chooses between them automatically.

  • static: compares CSS rules against the delivered DOM. No browser, runs in milliseconds. Built for SSG, SSR, or MPA output that already has real markup.
  • render: loads the page in a real viewport and measures what actually paints above the fold (uses Playwright). Built for SPA shells or when you want a tight, precise set per viewport.

It’s a fundamental change. Until v8, Critical used Penthouse internally, and Penthouse always launched a headless browser. Now the first thing it tries is a browser-free pass.

Also:

  • Lighter core: rebuilt on lightningcss, css-tree, and linkedom, with three runtime dependencies. Playwright is an optional peer dependency that’s imported only when needed, so the default install doesn’t download any browser.
  • Deterministic output: the same input produces identical output byte for byte.
  • Compatible with strict CSP: doesn’t add inline scripts.
  • New API: v9 is a major version with new API and options. If you’re coming from v8, check the README instead of porting your config as-is.

It also includes an MCP server and a structured report on each run, designed for code agents and build scripts.

How does Critical choose an engine?

With engine: "auto" (the default), Critical inspects the delivered HTML. If the document already contains rendered content, it uses the static engine. If it’s an empty shell—for example <div id="root"></div> with no markup yet—there’s nothing to compare without a browser, so it switches to the render engine. It always tells you which engine it used and why, and you can lock it with engine: "static" or engine: "render".

Can you extract critical CSS without a browser?

Yes, as long as your HTML already contains the page’s markup: static site generators, server-rendered pages, and classic multipage sites.

One caveat: without a browser there’s no fold. By default, the static engine inserts all used CSS, not just the first screen. To fine-tune it without a browser, add data-critical-fold to the container that wraps the first-screen content and the static engine will limit comparison to that subtree.

How do you install Critical 9?

Install it as a dev dependency:

npm install --save-dev critical

That’s enough for the static engine. The render engine also needs Playwright and a browser:

npm i -D playwright && npx playwright install chromium

CLI

# Optimizes a build directory in place (inserts critical CSS and defers the rest)
critical ./dist --inline --write

# Shows what it would do and why, without writing anything
critical ./dist --explain

# Forces a pass with a real browser for an SPA
critical app.html -e render -i > out.html

# Union of mobile and desktop folds
critical page.html --dimensions 390x844,1300x900 -i

The default viewport for the render engine is 1300×900; you can change it with -w and -h, or use --dimensions for multiple at once.

API

import { critical } from "critical";

const { html, css, report } = await critical({
  src: "dist/index.html",
  inline: true,
});

critical() never writes to disk on its own: it returns the result and you decide where to save html and css.

Docker

The official image includes Playwright and Chromium, so both engines work with no extra setup:

docker run --rm -v "$PWD/dist:/site" ghcr.io/addyosmani/critical:master \
  critical . --inline --write

Does Critical work with Node 20?

No. Critical 9 requires Node.js ≥ 22.13, so Node 20 and the early 22.x versions are out. If your CI image is still on Node 20, stay on the v8 line or upgrade the runtime before upgrading Critical.

When to use the rendering engine instead of the static one?

Use the rendering engine in three cases:

  • Unrendered SPA shells. If the HTML at build time is just <div id="root"></div>, there’s nothing to compare statically. Automatic mode will pick render anyway; setting it just skips the check.
  • Long pages. The static engine inserts all used CSS, so footer styles, FAQs, or pricing tables can end up in the blocking payload. The rendering engine knows where the fold is and leaves them out.
  • Multiple viewports. With --dimensions you get a single critical set that covers mobile and desktop.

For everything else, the static path is faster and keeps browsers out of your pipeline.

Critical or Beasties?

It depends on how your site is rendered. Beasties—the maintained successor to Google’s Critters and the engine behind optimizeCss in Next.js—also runs without a browser and is the fastest option for prerendered pages.

Critical’s maintainers publish a comparison in the repo itself (COMPARISON.md) that openly acknowledges when Beasties is better: simple SSG/SSR sites, especially if your framework already integrates it. Critical, with its rendering engine, fits when you need a precise critical set per viewport, have long pages, or work with SPA shells you don’t want to prerender separately.

Their benchmark shows an inserted payload 63.8% smaller than Beasties on a long page. It’s a figure published by the maintainers themselves, who also note it depends on the test case: it’s just a couple of pages, not a corpus. The structural idea does hold: an engine that knows the viewport doesn’t insert CSS below the fold, so its payload doesn’t grow with page length.

At the time of this post, Critical had no official plugins for Vite, webpack, Nuxt, or Next (their own comparison marks them as “not yet”): it runs as a post-build step, from the CLI or API.

How to use Critical in CI?

Two properties make Critical 9 fit well in a build pipeline:

  • Determinism. The output is identical byte for byte for the same input, so you can version it and review diffs without noise. --json emits a machine-readable report: engine used, rules preserved, blocking bytes removed, warnings, and time.
  • CSP compatibility. Deferred stylesheets are replaced by a <link rel="preload"> in the <head> and the actual sheet link moves to the end of the <body>. There are no inline event handlers or fallback <noscript>, so it works with a strict script-src policy. For a strict style-src, pass inline: { nonce: "…" } and the inserted <style> will carry your nonce. If you want a sheet left untouched, add data-critical-skip to its <link>.

If you orchestrate builds from an agent, Critical also exposes an MCP server with the optimize_critical_css tool, which returns the same report.

Looking for more CSS tools? Check out our selection of 70 hand-picked CSS resources.