Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

minimalcss

peterbe80kMIT0.11.3

Extract the minimal CSS used in a set of URLs with puppeteer

css, web-perf, web-performance, optimizer

readme

minimalcss

Build status NPM version styled with prettier Renovate enabled

A Node library to extract the minimal CSS used in a set of URLs with puppeteer. Used to find what minimal CSS is needed to render on first load, even with document.onload executed.

This minimal CSS is also known as critical path CSS and ultimately a web performance technique to make web pages load faster at initial load.

What does it do

You supply a list of URLs that it opens (one at a time) and for each page it downloads all external CSS files (e.g. <link rel="stylesheet" href="bootstrap.min.css">) and uses the DOM and document.querySelector to investigate which selectors, in the CSS, are actually in the DOM. That minimal payload of CSS is all you need to load the URLs styled without having to make it block on CSS.

Under the hood it relies on the excellent puppeteer library which uses the Headless Chome Node API. This means it runs (at the time of writing) Chrome 62 and this library is maintained by the Google Chrome team.

The CSS to analyze (and hopefully minimize) is downloaded automatically just like a browser opens and downloads CSS as mentioned in the DOM as <link> tags.

The CSS is parsed by CSSTree and the minification and compression is done with CSSO. An AST of each CSS payload is sent into the Headless Chrome page evaluation together with a callback that compares with the DOM and then each minimal CSS payload is concatenated into one big string which then CSSO compresses into one "merged" and minified CSS payload.

Usage

Install:

yarn add minimalcss --dev

You can install it globally if you like:

yarn global add minimalcss
npm install [--save-dev|--global] minimalcss

Now you can run it:

./node_modules/.bin/minimalcss https://example.com/ https://example.com/aboutus > minimal.min.css

Prior art

minimalcss isn't the first library to perform this task. What's unique and special about minimalcss is that it uses the Chrome Headless browser.

  • penthouse - uses puppeteer (since version 1.0) and CSSTree. Supports only 1 URL at a time and can't you have to first save the CSS files it should process.

  • critical - uses penthouse (see above) with its "flaws" meaning you can only do 1 URL (or HTML string) and you have to prepare the CSS files too.

  • UnCSS - uses jsdom to render and execute JavaScript. Supports supplying multiple URLs but still requires to manually supply the CSS files to process.

  • mincss - Python project that uses lxml.html to analyze the HTML statically (by doing a GET of the URL as if done by a server). I.e. it can't load the HTML as a real browser would and thus does not support a DOM with possible JavaScript mutations on load. It can optionally use PhantomJS to extract the HTML.

Killer features

  • You don't need to specify where the CSS is. It gets downloaded and parsed automatically.

  • It uses puppeteer and CSSTree which are both high quality projects that are solid and well tested.

  • The CSS selectors downloaded is compared to the DOM before and after JavaScript code has changed the DOM. That means you can extract the critical CSS needed to display properly before the JavaScript has kicked in.

  • Ability to analyze the remaining CSS selectors to see which keyframe animations that they use and use this to delete keyframe definitions that are no longer needed.

  • You can specify a viewport, which might cause the page to render slightly different. It does not create the minimal CSS only on DOM that is visible though.

  • If the CSS contains @font-face { ... } rules whose name is never used in any remaining CSS selector, the whole font-face block is removed.

Help needed

Let's make this a thriving community project!

Help needed with features, tooling, and much testing in real web performance optimization work.

API

const minimalcss = require('minimalcss');

Get version minimalcss.version

Just prints out the current version.

Run a minimization minimalcss.run(options)

Returns a promise. The promise returns an object containing, amongst other things, the minified minimal CSS as a string. For example:

minimalcss
  .minimize({ url: 'https://example.com/' })
  .then(result => {
    console.log('OUTPUT', result.finalCss.length, result.finalCss);
  })
  .catch(error => {
    console.error(`Failed the minimize CSS: ${error}`);
  });

That result object that is returned by the minimize function contains:

  • finalCss - the minified minimal CSS as a string.
  • stylesheetContents - an object of stylesheet URLs as keys and their content as text.

Optionally, you can supply a list of URLs like this:

minimalcss
  .minimize({ urls: ['https://example.com/page1', 'https://example.com/page2'] })
  ...

and minimalcss will try to merge the minimal critical CSS across all pages. But we aware that this can be "dangerous" because of the inherit order of CSS.

API Options

Calling minimalcss.run(options) takes an object whose only mandatory key is urls or url. Other optional options are:

  • debug - all console logging during page rendering are included in the stdout. Also, any malformed selector that cause errors in document.querySelector will be raised as new errors.
  • skippable - function which takes request as an argument and returns boolean. If it returns true then given request will be aborted (skipped). Can be used to block requests to Google Analytics etc.
  • loadimages - If set to true, images will actually load.
  • withoutjavascript - If set to false it will skip loading the page first without JavaScript. By default minimalcss will evaluate the DOM as plain as can be, and then with JavaScript enabled and waiting for network activity to be idle.
  • disableJavaScript - By default JavaScript is enabled. If set to true it will ignore withoutjavascript option and loading the page only one time without JavaScript.
  • browser - Instance of a Browser, which will be used instead of launching another one.
  • userAgent - specific user agent to use (string)
  • viewport - viewport object as specified in page.setViewport
  • puppeteerArgs - Args sent to puppeteer when launching. List of strings for headless Chrome.
  • cssoOptions - CSSO compress function options
  • timeout - Maximum navigation time in milliseconds, defaults to 30 seconds, pass 0 to disable timeout.
  • ignoreCSSErrors - By default, any CSS parsing error throws an error in minimalcss. If you know it's safe to ignore (for example, third-party CSS resources), set this to true.
  • ignoreJSErrors - By default, any JavaScript error encountered by puppeteer
  • ignoreRequestErrors - When CSS files return 404 or another request error minimalcss will ignore this instead of throwing an error. will be thrown by minimalcss. If you know it's safe to ignore errors (for example, on third-party webpages), set this to true.
  • styletags - If set to true, on-page <style> tags are parsed along with external stylesheets. By default, only external stylesheets are parsed.
  • enableServiceWorkers - By default all Service Workers are disabled. This option enables them as is.
  • whitelist - Array of css selectors that should be left in final CSS. RegExp patterns are supported (e.g. ['sidebar', icon-.*, .*-error]).

Warnings

Google Fonts

Suppose you have this in your HTML:

<link href="https://fonts.googleapis.com/css?family=Lato" rel="stylesheet">

then, minimalcss will consider this an external CSS stylesheet, load it and include it in the minimal CSS.

The problem is that Google Fonts will respond to that URL dynamically based on the user agent. In other words a different CSS payload depending on who's asking. So, the user agent when minimalcss runs will be whatever puppeteer uses and it might not be the best CSS for other user agents. So to avoid this predicament use the skippable option. On the command line you can do that like this:

./node_modules/.bin/minimalcss --skip fonts.googleapis.com https://example.com

With the API, you can do it like this:

minimalcss
  .minimize({
    url: 'https://example.com',
    skippable: request => {
      return !!request.url().match('fonts.googleapis.com');
    }
  })
  .then(result => {
    ...
  });

Multiple URLs

minimalcss can accept multiple URLs when figuring out the minimal CSS for all those URLs, combined. But be careful, this can be dangerous. If you have one URL with this HTML:

<head>
  <link rel="stylesheet" href="base.css">
  <link rel="stylesheet" href="specific.css">
</head>

and another URL with...:

<head>
  <link rel="stylesheet" href="base.css">
</head>

When combining these, it will optimize the CSS in this order:

  1. base.css
  2. specific.css
  3. base.css

But if specific.css was meant to override something in base.css in the first URL, that might get undone when base.css becomes the last CSS to include.

See this issue for another good example why running minimalcss across multiple URLs.

About cheerio

When minimalcss evaluates each CSS selector to decide whether to keep it or not, some selectors might not be parseable. Possibly, the CSS employs hacks for specific browsers that cheerio doesn't support. Or there might be CSS selectors that no browser or tool can understand (e.g a typo by the CSS author). If there's a problem parsing a CSS selector, the default is to swallow the exception and let the CSS selector stay.

Also by default, all these warnings are hidden. To see them use the --debug flag (or debug API option). Then the CSS selector syntax errors are printed on stderr.

About @font-face

minimalcss will remove any @font-face rules whose name is not mentioned in any of the CSS selectors. But be aware that you might have a @font-face { font-family: MyName; } in some /static/foo.css but separately you might have an inline style sheet that looks like this:

<style type="text/css">
div.something { font-family: MyName; }
</style>

In this case the @font-face { font-family: MyName; } would be removed even though it's mentioned from somewhere else.

About Blobs

If your document uses Blob to create injectable stylesheets into the DOM, minimalcss will not be able to optimize that. It will be not be included in the final CSS.

Development

First thing to get started, once you've cloned the repo is to install all the dependencies:

yarn

Testing

Testing is done with jest. At the beginning of every test, a static file server is started on localhost and a puppeteer browser instance is created for every test.

To run the tests:

yarn jest

Best way to get into writing tests is to look at existing tests and copy.

Prettier

All code is expected to conform with Prettier according to the the .prettierrc file in the root of the project.

To check that all your code conforms, run:

yarn lintcheck

Use without a server

This blog post demonstrates technique to use minimalcss when you don't yet have a server. Using the http-server package you can start a server right before you run and shut down as soon as you're done.

License

Copyright (c) 2017-2020 Peter Bengtsson. See the LICENSE file for license rights and limitations (MIT).

changelog

0.11.3

  • Cope with selectors with a \ character before the : pull#426

0.11.2

  • Add a .npmignore to avoid shipping tests and backup files.

0.11.1

  • Upgrade sub-dependency find-my-way pull#407

0.11.0

  • The main parameter urls can no be a string called just url. You just need to either use urls (array) or url (string).

  • New ignoreRequestErrors option to ignore HTTP responses that are >=400. pull#365 Thanks @Fgruntjes

  • (Chore) Switching from TravisCI to GitHub Actions

0.10.0

  • Upgrade dependency on puppeteer to ^2.0.0

0.9.0

  • Ability to pass a list of whitelist selector regexes pull#344 Thanks @AlexDubok

  • Upgrade dependency on css-tree to 1.0.0-alpha.37

  • Upgrade dependency on csso to ~4.0.2

0.8.3

  • You can now render exclusively with JavaScript disabled. pull#312 Thanks @VladislavTkachuk

0.8.2

  • Big optimization. By looking up the "parent CSS selector" pre-emptively it can quickly discard repeated "child CSS selectors". pull#296

0.8.1

  • Ignore <link rel="preload"> and <link rel="prefetch"> tags whose href value was a .css extension. pull#275 Thanks @nicolas-t

0.8.0

  • By default, Service Workers are disabled. This new feature requires an upgrade of puppeteer to ^1.8.0. pull#265

  • Ability to also extract the CSS of all style tags with the styletags option. pull#260 Thanks @jc275

0.7.10

  • Not crash if the CSS contains multiple semicolons which crashes csso (https://github.com/css/csso/issues/378) pull#259 Thanks @jc275

  • Stylesheet link tags whose href URL contains a #fragment-example would cause an error because puppeteer doesn't include it in the response.url(). pull#255 Thanks @jc275

0.7.9

  • New option ignoreJSErrors to ignore possible JavaScript errors. pull#253 Thanks @jc275

0.7.8

  • New option ignoreCSSErrors to ignore possible CSS parsing errors. pull#249 Thanks @stereobooster

0.7.7

  • Throw explicit errors on invalid CSS pull#237 Thanks @harrygreen

0.7.6

  • List what timed out. Useful for debugging which resources failed. pull#199 Thanks @stereobooster

  • Upgrade to puppeteer 1.4.0 pull#214

0.7.5

  • Ability to pass an object of options to csso.compress() pull#167 Thanks @usebaz

0.7.4

  • Fix for logic of using the --withoutjavascript argument. pull#163 Thanks @stereobooster

  • Upgrade puppeteer dependency to version 1.2.

  • Fix for 304 Not Modified responses that actually don't redirect. pull#165 Thanks @stereobooster

0.7.3

  • Fix for pages that uses data:text/css ... as the href i <link> tags. pull#159

0.7.2

  • Data URIs in external stylesheets lost the data: part. pull#153 Thanks @stereobooster and @phiresky for reporting.

0.7.1

  • Any query strings in URLs in CSS is now preserved. pull#148 Thanks @usebaz

0.7.0

  • Important fix for how multiple external stylesheets are parsed in the exact order the <link rel=stylesheet> tags appear in the HTML. pull#131

  • The response interceptor skips or includes resources based on responseType instead of URL and filename. pull#118 Thanks @stereobooster

0.6.3

  • Redirects, both of external style sheets and other URLs is now correctly followed. pull#106

  • Remove @media print rules. pull#101

  • Switching to wait for networkidle0 instead to allow the page slightly more time to finish more XHR and static resources. pull#87

0.6.2

  • All @font-face rules whose name is never mentioned in any remaining selector is now deleted. pull#81

  • Rules inside keyframe at-rules are not analyzed. pull#83

0.6.1

  • Much better error handling. If a CSS file fails to download or some JavaScript on the page throws an error, the minimalcss process now exits immediately, closes the puppeteer instance, and triggers the rejection on the main promise. Thanks @stereobooster pull#65

0.6.0

  • Supports setting viewport. Both via the cli and via the pure API. Thanks @stereobooster pull#64 And works on the cli by passing a JSON string pull#78

0.5.1

  • Works with and requires puppeteer 1.0.0. pull#74 Thanks @jonathaningram

0.5.0

  • Engine massively refactored by the author of csstree and csso himself; @lahmatiy

  • The minimalcss.minimize() functions promise no longer contains a stylesheetAstObjects objects. It wasn't clear which AST it should be. Thanks again @lahmatiy

  • Redundant and never referred to keyframes get automatically removed. pull#57.

  • greenkeeper.io now helps maintain dependency upgrades.

0.4.0

  • Every URL you pass gets loaded twice. First without Javascript and then with JavaScript (and waiting for network to be idle). These means the minimal CSS will contain CSS that was necessary before the page is fully loaded as well. Also, the engine has entirely changed. Instead of evaluating the DOM inside a page evaluation (the equivalent of running in the Web Console), puppeteer is only used to 1) download relevant assets and 2) yield the DOM as a string of HTML. Instead cheerio is used to compare the CSS to the DOM. pull#53

0.3.1

  • Any errors raised internally by document.querySelector() are not swallowed unless run with options.debug == true pull#40

0.3.0

  • Option to override user agent used by puppeteer. pull#37 Thanks @stereobooster

  • Correction of relative URLs in CSS fixed. E.g. url(images/img.png) in /styles/main.css now becomes url(/styles/images/img.png) pull#28 Thanks @stereobooster

  • New option browser if you already have a puppeteer Browser instance you can pass that in. pull#36 Thanks @stereobooster

  • Errors thrown if any necessary .css download can't be found. pull#27 Thanks @stereobooster

  • New repeatable string argument --skip to cli to selectively skip downloading certain URLs. pull#31

0.2.4

  • Ability to pass a function skippable which can help cancel certain network request. pull#20 Thanks @stereobooster

  • Option to actually load images if you need it to. #26

0.2.3

compare

  • Don't choke on blob: stylesheet link objects. Thanks @stereobooster

  • Use TypeScript to do type checking for development. Thanks @stereobooster

0.2.2

compare

  • Correctly ignore all request that are images by extension.

0.2.1

compare

  • Important fix for parsing all media queries.

0.2.0

compare

  • The main minimize function returns an object (which contains .finalCss) instead of just the CSS. Useful to be able to see the stylesheets it parsed.

  • debug option which adds all console.log that happens to stdout. Off by default.

  • Upgrade to css-tree 1.0.0-alpha24

  • List of "dead obvious" selectors that don't need to be analyzed like *, body, and html.

  • Clean up repeated important-comments in the concatenated CSS.

  • Updated README with example how to use a catch.

0.1.2

  • Trivial package refactoring.

0.1.1

  • Better error handling on failed page navigation

0.1.0

  • Basic CLI working.