used-styles
Get all the styles, you have used to render a page.
(without any puppeteer involved)
Bundler and framework independent CSS part of SSR-friendly code splitting
Detects used css
files from the given HTML, and/or inlines critical styles. Supports sync or stream rendering.
Read more about critical style extraction and this library: https://dev.to/thekashey/optimising-css-delivery-57eh
- 🚀 Super Fast - no browser, no jsdom, no runtime transformations
- 💪 API - it's no more than an API - integrates with everything
- 🤝 Works with
strings
and streams
- ⏳ Helps preloading for the "real" style files
Works in two modes:
- 🚙 inlines style rules required to render given HTML - ideal for the first time visitor
- 🏋️♀️inlines style files required to render given HTML - ideal for the second time visitor (and code splitting)
Critical style extraction:
- 🧱 will all all used styles at the beginning of your page in a string mode
- 💉 will interleave HTML and CSS in a stream mode. This is the best experience possible
How it works
- Scans all
.css
files, in your build
directory, extracting all style rules names. - Scans a given
html
, finding all the classes
used. - Here there are two options:
3a. Calculate all style rules you need to render a given HTML. 3b. Calculate all the style files you have
send to a client.
- Injects
<styles>
or <links>
- After the page load, hoist or removes critical styles replacing them by the "real" ones.
Limitation
For the performance sake used-styles
inlines a bit more styles than it should - it inlines everything it would be "not
fast" to remove.
- inlines all
@keyframe
animations - inlines all
html, body
and other tag-based selectors (hello css-reset) - inlines all rules matching last part of a selector (
.a .b
would be included if .b
was used but .a
was not)
Speed
Speed, I am speed!
For the 516kb page, which needs 80ms to renderToString
(React) resulting time for the getCriticalRules
(very
expensive operation) would be around 4ms.
API
Discovery API
Use it to scan your dist
/build
folder to create a look up table between classNames and files they are described in.
- (Deprecated)
discoverProjectStyles(buildDirrectory, [filter]): StyleDef
- generates class lookup table
you may use the second argument to control which files should be scanned
filter
is very important function here. It takes fileName
as input, and returns
false
, true
, or a number
as result. False
value would exclude this file from the set, true
- add it,
and number
would change the order of the chunk. Keeping chunk ordered "as expected" is required to preserve style declaration
order, which is important for many existing styles.
const styleData = discoverProjectStyles(resolve('build'), (name) => {
const match = name.match(/(\d)_c.css/);
return match && +match[1];
});
⚠️ generally speaking - this approach working only unless there are no order-sensive styles from different chunks applied to a single DOM Element.
Quite often it never happen, but if you are looking for a better way - follow to #26 ☣️
loadStyleDefinitions
is a "full control API", and can used to feed used-styles
with any custom data, for example
providing correct critical css extraction in dev mode (no files written on disk)
return loadStyleDefinitions(
async () => cssFiles,
(file) => fetchTxt(`http://localhost:${process.env.DEV_SERVER_PORT}/${file}`)
);
Scanners
Use to get used styled from render result or a stream
-
getUsedStyles(html, StyleDef): string[]
- returns all used files, you will need to import them
-
getCriticalStyles(html, StyleDef) : string
- returns all used selectors and other applicable rules, wrapped
with style
-
getCriticalRules(html, StyleDef): string
- the same, but without <style>
tag, letting you handle in a way you
want
-
createStyleStream(lookupTable, callback(fileName):void): TransformStream
- creates Transform stream - will
inject <links
-
createCriticalStyleStream(lookupTable, callback(fileName):void): TransformStream
- creates Transform stream - will
inject <styles
.
React
There are only two things about react:
- to inline critical styles use another helper -
getCriticalRules
which does not wrap result with style
letting you
do it
import { getCriticalRules } from 'used-styles';
const Header = () => (
<style data-used-styles dangerouslySetInnerHTML={{ __html: getCriticalRules(markup, styleData) }} />
);
- React produces more valid code, and you might enable optimistic optimization, making used-styles a bit faster.
import { enableReactOptimization } from 'used-styles';
enableReactOptimization();
Example
Static rendering
There is nothing interesting here - just render, just getUsedStyles
.
import {discoverProjectStyles, getUsedStyles} from 'used-styles';
const stylesLookup = discoverProjectStyles('./build');
async function MyRender() {
await stylesLookup;
const markup = ReactDOM.renderToString(<App/>)
const usedStyles = getUsedStyles(markup, stylesLookup);
usedStyles.forEach(style => {
const link = `<link rel="stylesheet" href="build/${style}">\n`;
const link = `<link rel="prefetch" as="style" href="build/${style}">\n`;
});
const criticalCSS = getCriticalStyles(markup, stylesLookup);
Any bulk CSS operations, both getCriticalStyles
and getUsedStyles
are safe and preserve the selector rule
order. You may combine both methods, to prefetch full styles, and inline critical CSS.
! Keep in mind - calling two functions is as fast, as calling a single one !
Stream rendering
Please keep in mind - stream rendering in NOT SAFE in terms of CSS, as long as it might affect the ordering of
selectors. Only pure BEM and Atomic CSS are "safe", just some random CSS might be not compatible. Please test
results before releasing into production.
If you do not understand why and how selector order is important - please do not use stream transformer.
Stream rendering is much harder, and much more efficient, giving you the best Time-To-First-Byte. And the second byte.
Stream rendering could be interleaved(more efficient) or block(more predictable).
Interleaved Stream rendering
In case or React rendering you may use interleaved streaming, which would not delay TimeToFirstByte. It's quite
similar how StyledComponents works
import {discoverProjectStyles, createLink, createStyleStream} from 'used-styles';
import MultiStream from 'multistream';
const stylesLookup = discoverProjectStyles('./build');
const readableString = string => {
const s = new Readable();
s.push(string);
s.push(null);
s._read = () => true;
return s;
};
async function MyRender() {
const htmlStream = ReactDOM.renderToNodeStream(<App/>)
await stylesLookup;
const styledStream = createStyleStream(stylesLookup, (style) => {
return createLink(`dist/${style}`)
});
const styledStream = createCriticalStyleStream(stylesLookup);
res.write(`<!DOCTYPE html><html><head><script defer src="client.js"></script>`);
const middleStream = readableString('</head><body><div id="root">');
const endStream = readableString('</head><body>');
const streams = [
middleStream,
styledStream,
endStream,
];
MultiStream(streams).pipe(res);
htmlStream.pipe(styledStream);
!! THIS IS NOT THE END !! Interleaving links and react output would break a client side rehydration, as long as _
injected_ links were not rendered by React, and not expected to present in the "result" HTML code.
You have to move injected styles out prior rehydration.
import { moveStyles } from 'used-styles/moveStyles';
moveStyles();
You might want to remove styles after rehydration to prevent duplication. Double check that corresponding real CSS is
loaded.
import { removeStyles } from 'used-styles/moveStyles';
removeStyles();
Block rendering
Not sure this is a good idea
Idea is to:
- push
initial line
to the browser, with the-main-script
inside - push all used
styles
- push some
html
between styles
and content
- push
content
- push
closing
tags
That's all are streams, concatenated in a right order. It's possible to interleave them, but that's is not expected buy
a hydrate
.
import { discoverProjectStyles, createStyleStream, createLink } from 'used-styles';
import MultiStream from 'multistream';
const lookup = await discoverProjectStyles('./build');
const readableString = (string) => {
const s = new Readable();
s.push(string);
s.push(null);
s._read = () => true;
return s;
};
const htmlStream = ReactDOM.renderToNodeStream(<App />);
const styledStream = createStyleStream(lookup, (style) => {
headerStream.push(createLink(`dist/${style}`));
headerStream.push(`<link href="dist/${style}" rel="stylesheet">\n`);
});
res.write(`<!DOCTYPE html><html><head><script defer src="client.js"></script>`);
const middleStream = readableString('</head><body><div id="root">');
const endStream = readableString('</head><body>');
const streams = [
headerStream,
middleStream,
styledStream,
endStream,
];
MultiStream(streams).pipe(res);
htmlStream.pipe(styledStream, { end: false });
htmlStream.on('end', () => {
headerStream.push(null);
styledStream.end();
});
This example is taken from Parcel-SSR-example
from react-imported-component.
Hybrid usage
The advanced pattern described in Optimizing CSS Delivery
article proposes to:
- inline critical CSS for a first time customers
- use cached
.css
files for recurring
This library does not provide a way to distinguish "one" cohort of customers from another, although, provides an API to
optimize the delivery.
- use
createCriticalStyleStream
/getCriticalStyles
to inline critical CSS - use
createStyleStream
/getUsedStyles
to use .css
files - use
alterProjectStyles
with filter
options to create two different sets of styles: not yet cache set
for critical
styles, and the cached ones for used
. - yes - you have to use or two transformers, or call two functions, one after another.
Theoretically - all styles "critical" now, are "cached" ones next view.
Performance
Almost unmeasurable. It's a simple and single RegExp, which is not comparable to the React Render itself.
Comparison
comparing with tools listed at Google's Optimize CSS Delivery
-
penthouse - a super slow puppetter based solution. No integration with a
real run time renderer is possible. Generates one big style block at the beginning of a file.
-
critical - a super slow puppetter based solution. Able to extract critical
style "above the fold".
-
inline-critical - slow jsdom based solution. Generates one big style
block at the beginning of a file, and replaces all other links
by async variants. However, it does not detect any
critical or used styles in provided HTML - HTML is used only as a output target. 👎
-
critters-webpack-plugun - is the nearest analog of used-styles, build
on almost same principles.
used-styles
is faster that libraries listed above, and optimized for multiple runs.
License
MIT