Neolithic Programming Machine

    used-styles
    TypeScript icon, indicating that this package has built-in type declarations

    2.4.1 • Public • Published

    used-styles


    Get all the styles, you have used to render a page.
    (without any puppeteer involved)

    Build Status NPM version

    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

    1. Scans all .css files, in your build directory, extracting all style rules names.
    2. Scans a given html, finding all the classes used.
    3. 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.
    4. Injects <styles> or <links>
    5. 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.

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

    // with chunk format [chunkhash]_[id] lower ids are potentialy should be defined before higher
    const styleData = discoverProjectStyles(resolve('build'), (name) => {
      // get ID of a chunk and use it as order hint
      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 ☣️

    1. 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(
      /*list of files*/ async () => cssFiles,
      /*data loader*/ (file) => fetchTxt(`http://localhost:${process.env.DEV_SERVER_PORT}/${file}`)
      /*filter and order */ // (file) => order.indexOf(cssToChunk[file])
    );

    Scanners

    Use to get used styled from render result or a stream

    1. getUsedStyles(html, StyleDef): string[] - returns all used files, you will need to import them

    2. getCriticalStyles(html, StyleDef) : string - returns all used selectors and other applicable rules, wrapped with style

    3. getCriticalRules(html, StyleDef): string - the same, but without <style> tag, letting you handle in a way you want

    4. createStyleStream(lookupTable, callback(fileName):void): TransformStream - creates Transform stream - will inject <links

    5. createCriticalStyleStream(lookupTable, callback(fileName):void): TransformStream - creates Transform stream - will inject <styles.

    React

    There are only two things about react:

    1. 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) }} />
    );
    1. React produces more valid code, and you might enable optimistic optimization, making used-styles a bit faster.
    import { enableReactOptimization } from 'used-styles';
    
    enableReactOptimization(); // just makes it a but faster

    Example

    Static rendering

    There is nothing interesting here - just render, just getUsedStyles.

    import {discoverProjectStyles, getUsedStyles} from 'used-styles';
    
    
    // generate lookup table on server start
    const stylesLookup = discoverProjectStyles('./build');
    
    async function MyRender() {
      await stylesLookup;// it is "thenable"
      // render App
      const markup = ReactDOM.renderToString(<App/>)
      const usedStyles = getUsedStyles(markup, stylesLookup);
    
      usedStyles.forEach(style => {
        const link = `<link  rel="stylesheet" href="build/${style}">\n`;
        // or
        const link = `<link rel="prefetch" as="style" href="build/${style}">\n`;
        // append this link to the header output or to the body
      });
    
    // or
    
      const criticalCSS = getCriticalStyles(markup, stylesLookup);
    
    // append this link to the header output

    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';
    
    // generate lookup table on server start
    const stylesLookup = discoverProjectStyles('./build'); // __dirname usually
    
    // small utility for "readable" streams
    const readableString = string => {
      const s = new Readable();
      s.push(string);
      s.push(null);
      s._read = () => true;
      return s;
    };
    
    async function MyRender() {
      // render App
      const htmlStream = ReactDOM.renderToNodeStream(<App/>)
    
      await stylesLookup;
      // create a style steam
      const styledStream = createStyleStream(stylesLookup, (style) => {
        // _return_ link tag, and it will be appended to the stream output
        return createLink(`dist/${style}`) // <link href="dist/mystyle.css />
      });
    
      // or create critical CSS stream - it will inline all styles
      const styledStream = createCriticalStyleStream(stylesLookup); // <style>.myClass {...
    
      // allow client to start loading js bundle
      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>');
    
      // concatenate all steams together
      const streams = [
        middleStream, // end of a header, and start of a body
        styledStream, // the main content
        endStream,    // closing tags
      ];
    
      MultiStream(streams).pipe(res);
    
    // start by piping react and styled transform stream
      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';
    
    // .....
    // generate lookup table on server start
    const lookup = await discoverProjectStyles('./build'); // __dirname usually
    
    // small utility for "readable" streams
    const readableString = (string) => {
      const s = new Readable();
      s.push(string);
      s.push(null);
      s._read = () => true;
      return s;
    };
    
    // render App
    const htmlStream = ReactDOM.renderToNodeStream(<App />);
    
    // create a style steam
    const styledStream = createStyleStream(lookup, (style) => {
      // emit a line to header Stream
      headerStream.push(createLink(`dist/${style}`));
      // or
      headerStream.push(`<link href="dist/${style}" rel="stylesheet">\n`);
    });
    
    // allow client to start loading js bundle
    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>');
    
    // concatenate all steams together
    const streams = [
      headerStream, // styles
      middleStream, // end of a header, and start of a body
      styledStream, // the main content
      endStream, // closing tags
    ];
    
    MultiStream(streams).pipe(res);
    
    // start by piping react and styled transform stream
    htmlStream.pipe(styledStream, { end: false });
    htmlStream.on('end', () => {
      // kill header stream on the main stream 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

    Install

    npm i used-styles

    DownloadsWeekly Downloads

    1,610

    Version

    2.4.1

    License

    MIT

    Unpacked Size

    154 kB

    Total Files

    160

    Last publish

    Collaborators

    • kashey