# Beasties: inline critical CSS without a headless browser

> Beasties is a maintained fork of Google's critters that inlines the CSS your document actually uses and lazy-loads the rest, and it does it by parsing the HTML rather than rendering it in a headless browser, which makes it fast but means it inlines more than strictly above the fold. It suits prerendered single page applications that need a quicker first paint.

**danielroe/beasties** — A library to inline your app's critical CSS and lazy-load the rest.

- Repository: https://github.com/danielroe/beasties
- Website: https://npmx.dev/beasties
- Stars: 708 · Forks: 31
- Language: TypeScript
- License: Apache-2.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/danielroe-beasties

## The one deliberate difference: no headless browser

Beasties is a plugin that inlines your app's critical CSS and lazy-loads the rest, and it is a maintained fork of GoogleChromeLabs/critters. The repository is authored by the Chromium authors, with Jason Miller, Janicklas Ralph and Daniel Roe listed as contributors.

The fork exists for one specific reason, and the readme is explicit that it is a little different from other options because it doesn't use a headless browser to render content. That tradeoff is what allows Beasties to be very fast and lightweight, and it also means Beasties inlines all CSS rules used by your document, rather than only those needed for above-the-fold content.

That is the trade in one sentence, and it deserves to be read carefully by anyone who has used a browser-based critical CSS tool. A headless browser can tell you what is visible in the first viewport. A parser cannot, so it includes everything the document references. For a page whose stylesheet is mostly used, the difference between the two approaches is small. For a page with a long marketing section below the fold, or a large rarely-used component, the inlined payload is larger than a browser-based tool would produce.

The speed argument is the compensating half. Running a headless browser in your build is slow and heavy, and Beasties trades some inlining precision for a build that stays fast and has few dependencies.

## Where it fits: prerendered SPAs and first paint

The design makes Beasties a good fit when inlining critical CSS for prerendered or SSR'd single page applications. It was developed to be an excellent complement to prerender-loader, combining to dramatically improve first paint time for most Single Page Applications.

That pairing is the clearest statement of intent in the readme. A prerenderer produces static HTML for a JavaScript application, and the problem it leaves behind is that the HTML arrives with a stylesheet reference, so the browser must fetch and parse CSS before it can paint the first screen. Inlining the styles that the prerendered HTML needs removes that round trip. The rest of the stylesheet becomes a lazy-loaded resource that does not block the first render.

The feature list is short and each item is concrete: fast with no browser and few dependencies, integration with Webpack through beasties-webpack-plugin, support for preloading and inlining critical fonts, pruning of unused CSS keyframes and media queries, and removal of the inlined CSS rules from the lazy-loaded stylesheets.

That last item is the one that keeps the output small. If the rules you inlined stayed in the external stylesheet as well, the browser would download them twice, and the inlining would be a net loss. Removing them from the lazy-loaded sheet is what makes the approach pay off.

The audience is therefore front-end engineers working on SSR or prerendering pipelines who have measured a first-paint problem and want it fixed at build time rather than at runtime.

## The core API, in one constructor and one call

The library surface is small, and the readme's example is the whole API.

```js
import Beasties from 'beasties'

const beasties = new Beasties({
  // optional configuration (see below)
})

const html = `
  <style>
    .red { color: red }
    .blue { color: blue }
  </style>
  <div class="blue">I'm Blue</div>
`

const inlined = await beasties.process(html)
```

The result shows the transformation in one line, with the unused .red rule dropped and the used .blue rule inlined into a style tag, minified as it goes.

The option list is where the real decisions live, and it is generated from the source by documentation.js, so it stays in step with the code. The properties cover path and publicPath for resolving stylesheet URLs, external for inlining styles from external stylesheets, remote for downloading and inlining remote stylesheets over http, https or protocol-relative URLs, and the two size thresholds, inlineThreshold for inlining external stylesheets smaller than a given size and minimumExternalSize for inlining a non-critical external stylesheet that would otherwise be below a size.

The remaining options control the output shape. pruneSource removes the inlined rules from the external stylesheet, mergeStylesheets merges the inlined stylesheets into a single style tag, additionalStylesheets is a glob for other stylesheets to consider when looking for critical CSS, reduceInlineStyles controls whether inline style tags are evaluated and rewritten to contain only critical CSS, and allowRules always includes rules matching the given selectors or patterns regardless of whether they match anything in the document.

allowRules is the one to know about. It is the escape hatch for a rule the parser cannot know is needed, such as a class applied by client-side JavaScript after hydration.

## Preload strategies, the noscript fallback and CSP nonces

Three options govern how the lazy-loaded CSS actually reaches the browser, and they are where most of the tuning happens.

The preload option chooses which preload strategy to use, and the examples in the readme set it to swap. The noscriptFallback option adds a noscript fallback to the JavaScript-based strategies, which matters for anyone whose users include search engine crawlers or text browsers, since a strategy that relies on script to insert the stylesheet will leave the page unstyled without it.

The nonce option is the newest-looking of the three and the most practically relevant. It takes a CSP nonce to set on every style and script element Beasties injects, or a function called once per document to derive it. The function form exists because a nonce has to be unique per response, and a build-time tool that injects a static string into every page produces a nonce that a strict Content Security Policy will reject.

A strict CSP deployment is a real constraint on any tool that rewrites the document head, and Beasties handles it explicitly rather than leaving you to discover the problem in production.

The other two font-related options are worth noting together: inlineFonts controls inlining of critical font-face rules, and the feature list also mentions supporting preloading and inlining of critical fonts. Font loading is often the actual reason a page feels slow to become readable, so treating the font rules separately from the rest of the critical CSS is a sensible design.

## Vite and webpack plugins, and the same options in both

Beasties ships with a plugin for each of the two major bundlers, and the readme is careful to say that the webpack plugin supports the same configuration options as the main package.

For Vite, the plugin is vite-plugin-beasties and the configuration is a plugin entry with an options object:

```js
// vite.config.ts
import { defineConfig } from 'vite'
import { beasties } from 'vite-plugin-beasties'

export default defineConfig({
  plugins: [
    beasties({
      // optional beasties configuration
      options: {
        preload: 'swap',
      }
    })
  ]
})
```

The plugin will process the output for your index.html and inline critical CSS while lazy-loading the rest.

For webpack the plugin is beasties-webpack-plugin, and the readme shows it as a diff against a webpack.config.js:

```diff
// webpack.config.js
+const Beasties = require('beasties-webpack-plugin');

module.exports = {
  plugins: [
+    new Beasties({
+      // optional configuration
+      preload: 'swap',
+    })
  ]
}
```

The plugin is CommonJS, which matches how webpack configs are usually written, and the options are passed at the top level of the constructor rather than nested under an options key as in the Vite case. That inconsistency is small but real if you are moving configuration between the two.

The result in both cases is the same: the resultant html has its critical CSS inlined and the stylesheets lazy-loaded.

## Project shape, tooling and how the fork is maintained

The repository is a pnpm workspace with a packages/ directory, and the root package.json is a private container named beasties-root. The build scripts target the two published packages separately, with build:main filtering the beasties package and build:webpack filtering beasties-webpack-plugin.

The tooling is modern and, for a fork, unusually thorough. Tests run through vitest with coverage, plus a separate type check with tsc --noEmit, a knip pass for unused code, and an installed-check that verifies dependency versions. Linting is eslint with the antfu config, and there is a simple-git-hooks pre-commit hook running lint-staged. The devEngines block pins Node to ^24.0.0 and pnpm to 12.14.0, and the pnpm-lock.yaml and pnpm-workspace.yaml sit at the root.

For a fork, the interesting question is who is doing the maintaining and how upstream changes get pulled in. The repository lists the Chromium authors as the author and three named contributors including Daniel Roe, who is also the owner of this repository path, which suggests the fork has a maintainer of its own rather than being a dormant copy. The readme does not describe the upstream sync process, so how often critters changes are pulled in is not something the documentation answers.

The licence is Apache-2.0, the repository is not archived, and the last push was on 2026-10-19. The recent releases are v0.5.5 from 2026-10-12, v0.5.4 from 2026-09-04 and v0.5.3 from 2026-09-02, so it is a 0.x line shipping every few weeks.

## Conclusion

Beasties fits a team shipping a prerendered or server-rendered single page application that measures a slow first paint and wants the critical stylesheet inlined at build time, and it does not fit a project whose problem is an oversized bundle rather than a blocking stylesheet. Before adopting it, check what it inlines on a page with below-the-fold content, since it inlines every rule the document uses rather than only the above-the-fold subset, and read the preload option, because the default strategy determines whether the rest of the CSS loads with a swap or a block.

## FAQ

### What is Beasties and how does it differ from critters?

Beasties is a maintained fork of GoogleChromeLabs/critters that inlines critical CSS and lazy-loads the rest. The difference is that it does not use a headless browser to render content, which makes it fast and lightweight, at the cost of inlining all CSS rules used by the document rather than only those needed above the fold.

### How do I install Beasties?

Install it as a development dependency with npm i -D beasties, or with yarn add -D beasties. The Vite and webpack integrations are published separately as vite-plugin-beasties and beasties-webpack-plugin.

### How do I use Beasties with Vite?

Add the beasties plugin from vite-plugin-beasties to the plugins array in your vite.config.ts, passing configuration under an options key, for example beasties({ options: { preload: 'swap' } }). The plugin then processes your index.html output, inlining the critical CSS and lazy-loading the rest.

### What does the allowRules option do in Beasties?

It always includes rules matching the given selectors or patterns in the critical CSS, regardless of whether they match elements in the document. It is the escape hatch for styles applied by client-side JavaScript after hydration, which the parser cannot see.

### Does Beasties work with a strict Content Security Policy?

There is a nonce option that sets a CSP nonce on every style and script element Beasties injects, and it accepts either a string or a function called once per document to derive the nonce, which matters because a nonce has to be unique per response.

## Sources

- [danielroe/beasties on GitHub](https://github.com/danielroe/beasties)
- [License: Apache-2.0](https://github.com/danielroe/beasties/blob/main/LICENSE)
- [Project website](https://npmx.dev/beasties)
- [README](https://github.com/danielroe/beasties/blob/main/README.md)
- [Releases](https://github.com/danielroe/beasties/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/danielroe-beasties
