• Stars
    star
    351
  • Rank 120,906 (Top 3 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created over 7 years ago
  • Updated almost 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

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

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).

More Repositories

1

premailer

Turns CSS blocks into style attributes
Python
1,041
star
2

mincss

Tool for finding out which CSS selectors you're NOT using.
Python
855
star
3

autocompeter

A really fast AJAX autocomplete service and widget
Python
277
star
4

django-static

Template tags for better serving static files from templates in Django
Python
194
star
5

django-cache-memoize

Django utility for a memoization decorator that uses the Django cache framework.
Python
158
star
6

github-pr-triage

A dashboard of Github Pull Requests
JavaScript
149
star
7

django-fancy-cache

A Django `cache_page` decorator on steroids.
Python
145
star
8

tornado-utils

Utility scripts for a Tornado site
Python
135
star
9

tiler

App for allowing you to host some huge ass photos on the web.
JavaScript
127
star
10

django-mongokit

Bridging Django to MongoDB with the MongoKit ODM (Object Document Mapper)
Python
122
star
11

govspy

Go vs. Python
Python
112
star
12

django-sockjs-tornado

Makes it easy to run a SockJS server in Django through Tornado.
Python
102
star
13

hashin

Helping you write hashed entries for packages in your requirements.txt
Python
98
star
14

whatsdeployed

What's deployed from a Github repo on various server environments?
JavaScript
84
star
15

python-gorun

Using (py)inotify to run commands when files change
Python
66
star
16

toocool

too cool for me
JavaScript
62
star
17

worklog

Tornado app behind DoneCal
JavaScript
59
star
18

django-peterbecom

Code for my personal home page
CSS
54
star
19

django-html-validator

A tool to do validation of your HTML generated from your Django app.
Python
47
star
20

tornado_gists

Collaborative collection of Tornado related Github gists
Python
39
star
21

htmltree

Finding out which DOM nodes weigh the most
CSS
28
star
22

premailer.io

A website that uses premailer
JavaScript
25
star
23

docsql

docsQL - Getting an overview over your Markdown file in your Jamstack site
TypeScript
22
star
24

grymt

Preps a set of HTML files for deployment
Python
20
star
25

groce

A mobile web app to help families do grocery and meal planning.
TypeScript
19
star
26

buggy

A client-side wrapper on the bugzilla.mozilla.org REST API.
JavaScript
16
star
27

optisorl

Django backend plugin for sorl-thumbnail that optimizes thumbnails
Python
14
star
28

fastestdb

A collection of benchmarks for which database is the fastest for Tornado
JavaScript
13
star
29

workon

Personally opinionated Todo list in React
JavaScript
9
star
30

minimalcss-website

Single-page-app website for showcasing minimalcss
JavaScript
9
star
31

minimalcss-server

Node Express server to wrap calling minimalcss
JavaScript
8
star
32

django-fastest-redis

Trying different configurations for django-redis
Python
8
star
33

bgg

A love story with Bugzilla, git and Github
Python
8
star
34

django-jingo-offline-compressor

Using jingo and django_compressor but miss offline compression?
Python
8
star
35

next-peterbecom

New front-end for www.peterbe.com
CSS
7
star
36

github-action-tricks

Tips and tricks to make you a GitHub Actions power-user
Shell
7
star
37

django-spellcorrector

Spellcorrector app for Django
Python
6
star
38

json-schema-reducer

Extract from a JSON/dict ONLY whats in the JSON Schema
Python
6
star
39

youshouldwatch-next

A to-watch list of movies and TV shows
TypeScript
6
star
40

IssueTrackerProduct

A bug/issue tracker for Zope2
Python
5
star
41

django-spending

Bengtsson Household Spending app
Python
5
star
42

remix-peterbecom

Front-end for www.peterbe.com in Remix
TypeScript
5
star
43

sockshootout

Comparing WebSockets vs. jQuery AJAX using Tornado
JavaScript
5
star
44

jest-ts-and-http

Node/TypeScript project that uses `jest` to test a local server
TypeScript
5
star
45

slimmer

slimmer
Python
5
star
46

headsupper

Houston. We have commits coming in.
CSS
5
star
47

peepin

Project superseded by https://github.com/peterbe/hashin
Python
5
star
48

howsmywifi

Measure (repeatedly) your broadband speed using Fast.com in a headless browser.
JavaScript
5
star
49

fake-failbot

Mock server to use locally instead of a real FailBot server
JavaScript
5
star
50

django-fastest-cache

Experiment to see speed of using various caching backends
Python
5
star
51

chiveproxy

Experimental React PWA
JavaScript
4
star
52

cheerio-to-text

Turning a Cheerio objects into plain text
TypeScript
4
star
53

ghdocs-goer

A VS Code Extension for people who contribute to https://github.com/github/docs by editing Markdown files in VS Code.
TypeScript
4
star
54

redunter

Hunting down unused CSS since 2015
Python
4
star
55

gg

Git and GitHub for the productivity addicted
Python
4
star
56

python-reverse-geocode-test

Comparing Google Reverse Geocoding against GeoNames
3
star
57

python-donecal

Python interface for the donecal.com restful HTTP API
Python
3
star
58

sockshootout2019

XHR vs WebSockets 2019
JavaScript
3
star
59

hylite

A CLI for syntax highlighting code to HTML
TypeScript
3
star
60

minimalcss2

A Node library to extract the minimal (critical) CSS based on a string of HTML and a string of CSS.
TypeScript
3
star
61

uslicenseplates

US License Plate Spotter
JavaScript
3
star
62

fwc_mobile

Python
3
star
63

esrun-tsnode-esno

ts-node vs. esrun vs. esno vs. bun
TypeScript
3
star
64

rpsls

Rock Paper Scissors Lizard Spock
3
star
65

render-block-images-in-css

Experimenting with inlining CSS images with data URLs
JavaScript
3
star
66

slowpage

Experimenting with browsers' resource download behaviour
Python
3
star
67

SMTPSink

Very basic script for running a SMTP service to see sent emails
Python
2
star
68

mdn-yari-content

All the MDN documents as index.html and index.yaml.
2
star
69

tornado_gkc

GKC (tornado)
JavaScript
2
star
70

minimalcss-cli

JavaScript
2
star
71

activity

An experiment with logging project activities
JavaScript
2
star
72

react-buggy

Side-project in need of a better name
JavaScript
2
star
73

FriedZopeBase

Misc utility Zope2 Product with nifty base classess
JavaScript
2
star
74

kl

Crosstips.org
Python
2
star
75

programmatically-render-next-page

Programmatically render a Next page without a server in Node
TypeScript
2
star
76

podcasttime2

client-side for podcastti.me
JavaScript
2
star
77

dinnerd

Experiment to plan weekly dinners
JavaScript
2
star
78

express-vs-fastify-vs-polka

Comparing Express, Polka, and Fastify for serving static assets
JavaScript
2
star
79

ZSQL

A Zope2 Product for executing SQL in a file based Zope2 product
Python
2
star
80

mdn-nottranslated

Actually NOT Translated on MDN?
JavaScript
2
star
81

fake-hydro

Receive Hydro send events locally
JavaScript
2
star
82

langdetect

Wrapping whatlanggo as a CLI in Go
Go
2
star
83

battleshits

You will never shit in peace
JavaScript
2
star
84

github-slideshow

A robot powered training repository ๐Ÿค–
HTML
2
star
85

primer-autocomplete

A standalone implementation of @primer/react Autocomplete to build a typehead search feature
TypeScript
2
star
86

ws-sane-demo

A playground to play with WebSockets and Sane.
JavaScript
1
star
87

gityouracttogether

Learning how to pretend a commit never happened
1
star
88

zope_products

Old ZOPE products
Python
1
star
89

justmerge

Automatically merge/land GitHub Pull Requests that are ready
Python
1
star
90

http-request-relay

Make HTTP request via distributed AWS Lambda functions
Python
1
star
91

gitbusy

What are you gitting busy with?
JavaScript
1
star
92

django-thuawood

A website for my mom.
Python
1
star
93

elmo-docs

Documentation for the Mozilla Elmo project
Python
1
star
94

django-stephanie

A website for my friend Stephanie Kearley Mรผller
CSS
1
star
95

gaffwall

canvas + slippy map
JavaScript
1
star
96

spellthese

"Train your own spell corrector with TextBlob" blog post demo code
HTML
1
star
97

mondayosaseri

1
star
98

lang-analyze

A scrappy script to analyze the state of translated documents in MDN.
JavaScript
1
star
99

classy

An online Bayesian classifier for anybody via a REST API
JavaScript
1
star
100

aroundtheworld

JavaScript
1
star