Skip to main content

Critical CSS deduplication

When server-side rendering, Archibald can inline the critical CSS of the matched bundles into a <style> tag in the document <head>. Because the same rules can appear in multiple bundles, the concatenated critical CSS often contains duplicates. The optimization.deduplicateCriticalCSS option removes those duplicates so the inlined <style> stays small.

Configuration

Set the option in your archibald.json:

{
"config": {
"optimization": {
"deduplicateCriticalCSS": "modern"
}
}
}

The option is tri-state:

ValueBehaviour
falseDisabled. The critical CSS is inlined as-is (default when the option is omitted).
"modern"Recommended. Uses the PostCSS-based optimizer that preserves every modern CSS feature.
trueDeprecated. Uses the legacy css-purge optimizer (crashes on modern CSS). Treated as "legacy".
"legacy"Deprecated. Uses the legacy css-purge optimizer (crashes on modern CSS).

When the deduplication runs, the inlined tag is marked with id="optimized-critical-styles" (otherwise id="critical-styles"). Deduplication only happens for production server-side renders.

What gets inlined

The critical <style> block inlines the CSS of the entry bundle, the modules rendered for the response, and the above-the-fold islands — components whose Loadable declares preloaded: true (header, navigation, …). Those islands render on the server, but their CSS lives in async chunks that only apply once the island's JS chunk loads on the client. Inlining their CSS into the critical block means it is applied on first paint, so the above-the-fold chrome does not flash unstyled before hydration (FOUC). When critical inlining is enabled the otherwise-redundant <link rel="preload" as="style"> hint for those islands is dropped; it is emitted only when critical inlining is off, to warm the stylesheet without blocking.

note

Island stylesheets are resolved from the build manifest by chunk name. When the bundler's SplitChunks logic divides a named async chunk into Name~0/Name~1/… variants, Archibald resolves all of them, so a split above-the-fold island still has its CSS inlined.

"modern" vs the legacy optimizer

The legacy optimizer (css-purge) relies on a CSS parser that predates modern CSS. On such input it raises a parse error and calls process.exit(1), which terminates the server process — so enabling true/"legacy" on a project that uses any of the following will crash production rendering rather than merely drop the rule:

  • container queries (@container, container-type)
  • cascade layers (@layer)
  • native CSS nesting
  • newer @supports syntax
danger

Because the failure is a process.exit, it cannot be caught by the renderer's try/catch. The only safe options are "modern" or false.

The "modern" optimizer is built on PostCSS and only ever rewrites whitespace and removes exact duplicates, so all of the above are preserved untouched. It:

  • merges sibling rules that share a selector, and sibling @media / @supports / @container / @layer blocks that share a prelude;
  • removes exact-duplicate declarations (same property and value), keeping the last one so the cascade is preserved;
  • keeps fallback declarations that share a property but differ in value (e.g. display: -webkit-box; display: flex;);
  • never merges rules across at-rule boundaries, so rules in different @container/@media scopes stay independent.

Example

The following block is preserved as-is by "modern" but is dropped by the legacy optimizer:

.card-container {
container-type: inline-size;
container-name: card;
}

@container card (min-width: 400px) {
.card {
font-size: 20px;
}
}

Deprecation

true and "legacy" are deprecated and log a warning at runtime. In v10, "modern" becomes the default behaviour for true. Migrate by setting optimization.deduplicateCriticalCSS to "modern". If you must keep the old behaviour after v10, set it explicitly to "legacy".