====
Bundler independent CSS part of SSR-friendly code splitting
Code splitting
This is all about code splitting, Server Side Rendering and React, even is React has nothing with this library.
Code splitting is a good feature, and SSR is also awesome, but then you have
to load all the use scripts
on the client, before making a page alive.
That's done, in a different ways. That's not a big task, as long as the usage of code splitted block is trackable - you are using it.
CSS is harder - you might just use random classes and what next? You are just importing CSS here and where, sometimes indirectly, and there is no way to understand whats happening.
While it's possible for webpack to add a
Link
to document header once someComponent
uses someStyle
, you can't do the same in the concurrent server environment - there is no to add a Link.
Code splitting libraries solved it straight forward - by building resource graph, and fetching all bound resources to that graph, but tracking is hard, and quite bound to the bundler, and could delay content sending.
Solution
- Scan all
.css
files, extracting all the style names. - Scan resulting
html
, finding all theclassNames
used - Calculate all the files you shall send to a client.
Bonus: Do the same for streams.
Bonus: Do it only for used styled
, not just imported somewhere.
API
Discovery API
Use to scan your dist
folder to create a look up table between classNames and files they are described in.
getProjectStyles(buildDirrectory)
- generates class lookup table
Scanners
Use to get used styled from render result or a stream
getUsedStyles(html): string[]
- returns all used filescreateStyleStream(lookupTable, callback(fileName):void): TransformStream
- creates Transform stream.
React
There is absolutely the same scanners, but for React
. Basically it's a simpler version of original scanner,
which rely on the "correct" HTML emitted from React, and just twice faster.
Example
Static rendering
There is nothing interesting here - just render, just getUsedStyles
.
import {getProjectStyles, getUsedStyles} from 'used-styles';
// or
import {getProjectStyles} from 'used-styles';
import {getUsedStyles} from 'used-styles/react';
// generate lookup table on server start
const lookup = getProjectStyles('./build');
// render App
const markup = ReactDOM.renderToString(<App />)
const usedStyles = getUsedStyles(markup, lookup);
usedStyles.forEach(style => {
const link = `<link href="build/${style}" rel="stylesheet">\n`;
// append this link to the header output
});
Stream rendering
Stream rendering is much harder. The idea is to make it efficient, and not delay Time-To-First-Byte. And the second byte.
Idea is to:
- push
initial line
to the browser, withthe-main-script
inside - push all used
styles
- push some
html
betweenstyles
andcontent
- 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 {getProjectStyles, createStyleStream, createLink} from 'used-styles';
import MultiStream from 'multistream';
// generate lookup table on server start
const lookup = await getProjectStyles('./build'); // __dirname usually
// small utility for "readable" streams
const readable = () => {
const s = new Readable();
s._read = () => true;
return s;
};
// render App
const htmlStream = ReactDOM.renderToNodeStream(<App />)
// create a style steam
const styledStream = createStyleStream(projectStyles, (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.
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 {getProjectStyles, createLink} from 'used-styles';
import {createStyleStream} from 'used-styles/react';
import MultiStream from 'multistream';
// generate lookup table on server start
const lookup = await getProjectStyles('./build'); // __dirname usually
// small utility for "readable" streams
const readable = () => {
const s = new Readable();
s._read = () => true;
return s;
};
// render App
const htmlStream = ReactDOM.renderToNodeStream(<App />)
// create a style steam
const styledStream = createStyleStream(projectStyles, (style) => {
// _return_ link tag, and it will be appened to the stream output
return createLink(`dist/${style}`)
});
// 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, // we dont need this stream
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 produce break client side rehydration, as long as injected links are not rendered by React, and not expected to present in the "result" HTML code.
You have to move injected styles prior rehydration.
import { moveStyles } from 'used-styles/moveStyles';
Performance
Almost unmeasurable. It's a simple and single RegExp, which is not comparable to the React Render itself.
License
MIT