Critical CSS
By default every page links its stylesheets: one <link rel="stylesheet">
per tier it reaches, cached under one URL for the whole site. criticalCss
turns that around for the pages you name — their stylesheets travel inside the
HTML, in <style> elements, and the links they replaced are gone.
Declare it in the build section of pagedeck.config.ts, as a map of page patterns
to flags:
build: {
outDir: "./site",
criticalCss: {
"/landing/**": true,
"/landing/legal": false,
},
// ...
}The field is optional, and a site that declares none behaves exactly as before: linked tiers everywhere.
What it is for
One thing: a cold-cache landing page. A visitor arriving from an ad has no
cache for your site, so every stylesheet in the <head> is a round trip that
blocks the first paint. Inlining removes those round trips at the cost of the
bytes travelling with the document.
Flag those pages, and leave the rest of the site alone.
What gets inlined
Everything the page would have linked: the font stylesheets from build.fonts
(the site-wide one and any route-scoped one that matches the page), the core
tier, any mid tier it reaches, its own page sheet, and every global stylesheet
from build.css.
That set is already "the page's critical CSS computed from its known component set" — the build knows exactly which components a page renders and which stylesheets they import, and it never runs a headless browser to guess what is above the fold. There is nothing narrower to inline, so the whole set goes in, in the same order it would have been linked: widest-shared first, so a narrower sheet can still override a wider one at equal specificity.
This includes the core sheet, and that is the trade. The core sheet is the one file the entire site shares and caches under one URL, and a page that inlines it does not get that cache entry — a visitor who continues to a second page downloads the core sheet there for the first time. On a cold-cache landing page there was no cache hit to lose, which is why the feature exists and why it is opt-in per page rather than a site-wide setting.
What it does not do
It does not remove the stylesheet files. Other pages still link them, and they are still emitted and deployed. If you flag every page of a site, the sheets ship in the deploy tree with nothing pointing at them — storage and upload time, never a request, since no document names them. Pruning an emitted file changes what the manifest promises about the tree, which is the deploy story's question rather than this feature's, so the files stay.
It cannot stop a hydrating island from fetching its sheet again. A page's
entry chunk carries Vite's preload helper, which inserts a <link> for a
dynamically imported chunk's stylesheet when the document does not already have
one. On an inlined page that request happens after first paint and changes
nothing about how the page looks — the rules are already there — but it is a
request. Flag pages that are mostly content; a page that is mostly islands has
less to gain here anyway.
The pattern language
Keys are the same page patterns build.budget uses — the same globs, the same
<locale>: scope, the same rule for which of several matching keys wins, and the
same refusal of a pair no page can choose between. They are read by the same
code, so the two fields cannot drift apart. See
JavaScript budgets for the language in full.
The value is true or false. false is how a page opts back out of a broader
pattern: in the example at the top, everything under /landing inlines except
/landing/legal, because the more specific key wins.
A pattern that matches no page inlines nothing, which is not an error — the same rule budgets follow.
What is refused
A bad key or a bad value is refused when the config loads, before anything is rendered, with every fault in one report:
Config "/site/pagedeck.config.ts": "build.criticalCss" declares 1 value that is not true or false — write true to inline a page's stylesheets into its HTML, or false to leave it linking them:
"/landing" — "yes"
Config "/site/pagedeck.config.ts": "build.criticalCss" holds 1 pair of patterns no page can choose between — make one of the pair more specific, or give both the same flag:
"/a/*" and "/*/b" — equally specific, and both match "/a/b"
One more refusal happens during the build, and it is worth knowing about: a
stylesheet holding the byte sequence </style cannot be inlined, because an
HTML parser would end the element there and read the rest of the sheet as
markup. The build says so rather than emitting the page, and names where the
sequence is so you do not have to search a minified file for it:
Critical CSS: 1 stylesheet cannot be inlined because it holds "</style", which ends the element early and puts the rest of the sheet into the page as markup — remove the "</style" sequence from the stylesheet, or drop the page from build.criticalCss so the sheet is linked instead:
"/assets/fw-core-DO-Blg1p.css" — inlined into en /landing, "</style" at line 1, column 4188
The bytes are otherwise inlined exactly as the bundler emitted them. The build never rewrites, re-minifies or reorders a stylesheet's contents.
In the size report
Flagging a page is enough to get .pagedeck/budget-report.json: the report is written
when the site declares budget, criticalCss, or both, and a flagged page has
a row whether or not a budget pattern matches it. The bytes appear in
cssInlined, weighed the same way css is — Brotli bytes of the stylesheets
themselves — so a flagged page and an unflagged one can be compared:
{
"locale": "en",
"path": "/landing",
"css": 0,
"cssInlined": 4188,
"html": 6023
}A row a flag earned carries no pattern, no limitText and no limit — the
three fields a budgeted row has. That is the point: a flag buys the measurement
and nothing that can fail a build.
css counts the sheets a page links, so it is 0 on a flagged page. Those
bytes also travel inside the document, so html grows too — the three figures
are not meant to be added up.
Like css and html, cssInlined is reported and never enforced: no amount of
CSS, inlined or linked, can fail a budget.