Extract & Inline Critical-path CSS in HTML pages
Extract the critical-path (above-the-fold) CSS from your HTML, inline it, and load the rest asynchronously — so the browser can paint the first screen without waiting on a full stylesheet.
Critical removes render-blocking CSS from the critical path, which is one of the most direct levers on Largest Contentful Paint and first render. It works for static sites, MPAs, and single-page apps, and it's built to be driven by humans, build pipelines, and coding agents alike.
npm install --save-dev critical
Given an HTML document and its stylesheets, Critical:
<style> at the top of <head> so first paint
needs only the HTML.<link rel="stylesheet"> is replaced in the head by a
<link rel="preload"> and moved to the end of the body, so it no longer blocks first paint.
No inline scripts are added, so it works under a strict Content Security Policy.The output is deterministic: the same input produces byte-identical output, so it's safe to run in CI and diff in version control.
# Optimize a build directory in place (inlines critical CSS, defers the rest)
critical ./dist --inline --write
# See what it would do and why, without writing anything
critical ./dist --explain
# A single file to stdout
critical index.html --inline > index.critical.html
import { critical } from "critical";
const { html, css, report } = await critical({
src: "dist/index.html",
inline: true,
});
// `html` — the document with critical CSS inlined and stylesheets deferred
// `css` — the critical CSS on its own (minified)
// `report` — structured diagnostics (engine used, bytes, rules, warnings, timing)
critical() never writes to disk on its own — it returns the result. Use the CLI's --write
/ --out, or write result.html / result.css yourself.
The container image includes Playwright and Chromium, so both the static and render engines work
without additional setup. Mount the generated site at /site and include critical before its
arguments:
docker pull ghcr.io/addyosmani/critical:master
docker run --rm -v "$PWD/dist:/site" ghcr.io/addyosmani/critical:master \
critical . --inline --write
Critical picks the right strategy for each document automatically (engine: "auto", the
default), and tells you which it used and why.
Automatic routing. When engine is "auto", Critical inspects the delivered HTML. If the
document already contains rendered content, the static engine is correct and runs in
milliseconds. If the document is an empty application shell (e.g. with
no markup yet), there's nothing to match against statically, so Critical escalates to the render
engine and measures the page the way a browser actually paints it.
You can always pin the engine explicitly with engine: "static" or engine: "render".
The render engine uses Playwright, declared as an optional peer dependency. It's imported lazily, so the static path — and the default install — never pull in a browser. To use the render engine:
npm i -D playwright && npx playwright install chromium.
critical <input> [options]
input a directory (all *.html are processed), an .html file, or stdin
Option
Description
Default
-e, --engine <auto|static|render>
Engine selection
auto
-i, --inline
Inline critical CSS and defer the rest
off
-w, --width <px>
Render-engine viewport width
1300
-h, --height <px>
Render-engine viewport height
900
--dimensions <WxH,WxH>
Multiple render viewports (e.g. 390x844,1300x900)
—
--no-fold
Ignore [data-critical-fold] scoping in the static engine
—
--no-minify
Keep the critical CSS readable instead of minifying
—
-o, --out <file|dir>
Write output here instead of stdout
stdout
--write
Rewrite the input file(s) in place (use with --inline)
off
--json
Emit the structured result as JSON
off
--explain
Print the engine decision and stats to stderr
off
--help
Show help
—
critical ./dist --inline --write # whole build dir, in place
critical index.html --explain # routing decision + size stats
critical app.html -e render -i > out.html # force a real-browser pass for an SPA
critical page.html --dimensions 390x844,1300x900 -i # union of mobile + desktop folds
cat page.html | critical --inline # stdin -> stdout
critical ./dist --json # machine-readable report for CI/agents
import { critical } from "critical";
critical(options) → Promise<{ html, css, report }>src
string
—
Path or URL to an HTML file. Provide src or html.
html
string
—
Raw HTML source. Takes precedence over src.
css
string | string[]
—
Extra CSS: file paths, globs, or raw CSS strings, beyond what the document links.
base
string
dir of src, else cwd
Base directory for resolving stylesheet/asset paths.
engine
"auto" | "static" | "render"
"auto"
Engine selection (see Two engines).
inline
boolean | object
false
Inline critical CSS and defer the rest. Pass an object to configure inlining.
minify
boolean
true
Minify the critical CSS (via Lightning CSS).
foldAware
boolean
true
Honor [data-critical-fold] scoping in the static engine.
width
number
1300
Render-engine viewport width.
height
number
900
Render-engine viewport height.
dimensions
Array<{width, height}>
—
Multiple render viewports; their critical sets are unioned. Overrides width/height.
timeout
number
30000
Render-engine navigation timeout (ms).
userAgent
string
—
User agent for the render engine.
The inline object accepts:
preload
boolean
true
Insert a <link rel="preload" as="style"> hint where each deferred sheet was.
nonce
string
—
Nonce to set on the injected <style>, for a strict style-src CSP.
const { html, css, report } = await critical({ src: "dist/index.html", inline: true });
html — the document. Identical to the input unless inline is set.css — the critical CSS, minified (unless minify: false).report — structured diagnostics:{
"engine": "static", // engine that actually ran
"reason": "rendered document (…) — matching used CSS without a browser",
"requestedEngine": "auto",
"rules": { "kept": 10, "total": 15 },
"bytes": { "stylesheets": 1107, "critical": 544, "savedBlocking": 1107 },
"stylesheetsDiscovered": ["/styles.css"],
"stylesheetsDeferred": ["/styles.css"], // present when inlined
"warnings": ["…"],
"durationMs": 13,
"deterministic": true,
}
report is designed to be read by a program: it explains the decision, quantifies the win
(savedBlocking = render-blocking bytes removed from the critical path), and surfaces any
caveats as warnings.
With inline: true, Critical:
Inserts <style data-critical>…</style> as the first child of <head> so it wins the
cascade race against the deferred sheets.
Stops each <link rel="stylesheet"> from blocking the first paint, using the preload strategy:
<link rel="preload" as="style" href="/styles.css" />
<link rel="stylesheet" href="/styles.css" />
The preload starts the download early; the stylesheet applies after the above-the-fold content
(already styled by the inlined critical CSS) has painted. There is no inline event handler,
so this works under a strict script-src Content Security Policy, and no <noscript>
fallback is needed — the stylesheet loads normally whether or not JavaScript runs.
Add data-critical-skip to a <link> to leave it untouched. For a strict style-src CSP, pass
inline: { nonce: "…" } to stamp the
No open issues yet, or sync has not completed.