kentcdodds / kentcdodds/tree-shake-css

Tree Shaking

Open
#1 1 comment 1 reaction 0 assignees View on GitHub
Dominant language
JavaScript
Stars
36
Forks
0
PR merge metrics
No merged PRs in 30d

Description

Heyo. I know this is a demo build, but I'm going to go a little deeper. Forgive me if you're already savvy on this. If not, its awesome to share knowledge with one of the superpowers in the JS world!

First stop, webpack optimization configuration is your friend. I use it for granular code-splitting, a special technique will allow you to spin up micro-frontend verticles which can use other builds resources on demand, async.

check how you can write functions to hook into `splitChunks` this is super powerful!
https://webpack.js.org/plugins/mini-css-extract-plugin/#extracting-css-based-on-entry

Then, as of late - there's the optimize css plugin. I actually use both, but you can combine cssnano configs into it.

```
new OptimizeCssAssetsPlugin({
assetNameRegExp: /\.optimize\.css$/g,
cssProcessor: require('cssnano'),
cssProcessorPluginOptions: {
preset: ['default', { discardComments: { removeAll: true } }],
},
canPrint: true
})
```

postcss-loader is my main tactic, but the more i work with the internal webpack team, the more im preparing to migrate.

heres the options i pass to css-nano

```
//postcss.config.js
const config = require('./config');

module.exports = {
ident: 'postcss',
sourcemaps: config.IS_DEV,
plugins: {
'postcss-import': {},
'postcss-flexbugs-fixes': {},
'postcss-preset-env': {
autoprefixer: {
browsers: ['last 4 versions', 'Safari >= 7', 'Firefox ESR', 'not ie < 9'],
flexbox: 'no-2009',
},
stage: 3,
},
cssnano: {
safe: true,
autoprefixer: true,
discardComments: {
removeAll: true,
},
calc: true,
colormin: true,
convertValues: true,
core: true,
discardDuplicates: true,
discardEmpty: true,
discardOverridden: true,
discardUnused: false,
filterOptimiser: true,
functionOptimiser: true,
mergeIdents: true,
mergeLonghand: true,
mergeRules: true,
minifyFontValues: true,
minifyGradients: true,
minifyParams: true,
minifySelectors: true,
normalizeCharset: true,
normalizeUrl: false,
orderedValues: true,
reduceBackgroundRepeat: true,
reduceIdents: true,
reduceInitial: true,
reducePositions: true,
reduceTimingFunctions: true,
reduceTransforms: true,
svgo: false,
uniqueSelectors: true,
zindex: true,
},
},
};
```

My builds are actually composable, the idea being "one build to rule them all", you can hack on or extend them, but all builds are built off eachother. Recently ive combined a couple tricks, so you might see some familiar stuff. But with a touch of webpack maintainer added to the mix

My loaders are exported out like this, because i usually have server, native, and all sorts of other conventions which may require on to use a slightly modified loader.

Extract-css-chunks is just mini-css but it supports HMR (we are merging with webpacks own project)

```
//loaders.js
const ExtractCSSChunks = require('extract-css-chunks-webpack-plugin');
const formatter = require('react-dev-utils/eslintFormatter');
const config = require('./config');

const babelLoader = {
test: /\.(js|mjs|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: [
{
// we do not resolve this because
loader: 'babel-loader',
options: {
cacheDirectory: true,
compact: !config.IS_DEV,
},
},
],
};

const eslintPreLoader = {
test: /\.(js|mjs|jsx)$/,
enforce: 'pre',
use: [
{
loader: config.requireResolve('eslint-loader'),
exclude: /node_modules/,
options: {
eslintPath: 'eslint',
cache: true,
formatter,
},
},
],
};

const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
{
loader: ExtractCSSChunks.loader,
},
{
loader: config.requireResolve('css-loader'),
options: cssOptions,
},
{
loader: config.requireResolve('postcss-loader'),
options: {
config: {path: path.join(__dirname, 'postcss.config.js')}
},
},
].filter(Boolean);
if (preProcessor) {
loaders.push({
loader: config.requireResolve(preProcessor),
options: {
sourceMap: config.IS_DEV,
},
});
}
return loaders;
};

const cssLoaderClient = {
test: /\.css$/,
exclude: /\.module\.css$/,
use: getStyleLoaders({
importLoaders: 2,
sourceMap: config.IS_DEV,
}),
sideEffects: true,
};

const urlLoaderClient = {
test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/,
loader: config.requireResolve('url-loader'),
options: {
limit: 10000,
name: '[name].[hash:8].[ext]',
},
};

const fileLoaderClient = {
test: /\.(png|jpe?g|gif|svg|webp)(\?.*)?$/,
use: [
{
loader: config.requireResolve('file-loader'),
options: {
name: '[name].[hash:7].[ext]',
publicPath: '/static/',
},
},
],
};

// Write css files from node_modules to its own vendor.css file
const externalCssLoaderClient = {
test: /\.css$/,
include: /node_modules/,
use: [ExtractCSSChunks.loader, config.requireResolve('css-loader')],
};

// Native ES6 Modules need to be interpreted correctly within webpack
// https://github.com/apollographql/react-apollo/issues/1737#issuecomment-372946515
const mjsLoader = {
test: /\.mjs$/,
include: /node_modules/,
type: 'javascript/auto',
};

const client = [
{
eslintPreLoader,
oneOf: [
mjsLoader,
babelLoader,
cssLoaderClient,
urlLoaderClient,
fileLoaderClient,
externalCssLoaderClient,
],
},
];

module.exports = {
client,
};
```
Lastly, ive got a production build which helps stitch things together

```
//client.prod.js

const merge = require('webpack-merge');
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const safePostCssParser = require('postcss-safe-parser');

const baseConfig = require('./client.base');
const config = require('./config');

module.exports = merge.smart(baseConfig, {
mode: 'production',
devtool: 'source-map',
output: {
devtoolModuleFilenameTemplate: info => path
.relative(config.appDirectory, info.absoluteResourcePath)
.replace(/\\/g, '/'),
},
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
parse: {
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
comparisons: false,
inline: 2,
},
mangle: {
safari10: true,
},
output: {
ecma: 5,
comments: false,
ascii_only: true,
},
},
parallel: true,
cache: true,
sourceMap: config.IS_DEV,
}),
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
parser: safePostCssParser,
map: false,
},
}),
],
splitChunks: {
chunks: 'async',
minSize: 10000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
cacheGroups: {
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all',
enforce: true
}
},
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
},
},
});
```

For better clairty, ive attached my entire build architecture. Pick and choose what you want.
Ill also follow up with additional insights into tree shaking webpack CSS, however this should allow you to generate cache groups and with this combination it should chunks out "shared" code and just load in one module on demand, as it does with JS tree shaking.

Natrually its harder without explicit exports and imports, but possible be either enabling or disabling SideEffects under the css loader. Ill need to dig a little deeper but wanted to give you something to work with. Any suggestions i put forth will likely be a small addition to this configuration :)

lemme know if theres any issues, i added a few things here without re-running a build as ive pulled out sensitive stuff. Worst case you need to npm install something.

[global-builds.zip](https://github.com/kentcdodds/tree-shake-css/files/2771780/global-builds.zip)

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by reading loaders.js, postcss.config.js, and client.prod.js, then run the production build described in the issue. Compare the current splitChunks, CSS loader, and optimization configuration with the linked webpack guidance. Done should be defined as a reproducible build that demonstrates the intended CSS and shared-code tree shaking behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
css, javascript, webpack
Domain
build-system, frontend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
18/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.