How to Fully Optimize Webpack 4 Tree Shaking

We reduced our bundle sizes by an average of 52%

Intro

The Benefits

No Code Repo

What is Considered Dead Code

// Importing, assigning to a JavaScript object, and using it in the code below
// This is considered "live" code and will NOT be tree-shaken
import Stuff from './stuff';doSomething(Stuff);// Importing, assigning to a JavaScript object, but NOT using it in the code below
// This is considered "dead" code and will be tree shaken
import Stuff from './stuff';doSomething();// Importing but not assigning to a JavaScript object and not using it in the code
// This is considered to be "dead" code and will be tree shaken
import './stuff';doSomething();// Importing an entire library, but not assigning to a JavaScript object and not using it in the code
// Oddly enough, this is considered to be "live" code, as Webpack treats library imports differently from local code imports
import 'my-lib';doSomething();

Writing Tree-Shakable Imports

// Import everything (NOT TREE-SHAKABLE)import _ from 'lodash';// Import named export (CAN BE TREE SHAKEN)import { debounce } from 'lodash';// Import the item directly (CAN BE TREE SHAKEN)import debounce from 'lodash/lib/debounce';

Basic Webpack Configuration

// Base Webpack Config for Tree Shakingconst config = {
mode: 'production',
optimization: {
usedExports: true,
minimizer: [
new TerserPlugin({...})
]
}
};

What Are Side Effects

How to Tell Webpack Your Code is Side-Effect Free

// All files have side effects, and none can be tree-shaken{
"sideEffects": true
}
// No files have side effects, all can be tree-shaken{
"sideEffects": false
}
// Only these files have side effects, all other files can be tree-shaken, but these must be kept{
"sideEffects": [
"./src/file1.js",
"./src/file2.js"
]
}

Global CSS and Side Effects

// Global CSS importimport './MyStylesheet.css';
// Webpack config for global CSS side effects ruleconst config = {
module: {
rules: [
{
test: /regex/,
use: [loaders],
sideEffects: true
}
]
}
};

What Are Modules, and Why Do They Matter

// Commonjs import/export module statementsconst stuff = require('./stuff');module.exports = stuff;// es2015 import/export module statementsimport stuff from './stuff';export default stuff;

Configuring Babel For es2015 Modules

// Basic babel config for es2015 modulesconst config = {
presets: [
[
'@babel/preset-env',
{
modules: false
}
]
]
};

And Now Jest Blows Up

Fixing Your Local Code for Jest

// Babel config for separate environments, we move the "preset" to the "env" sectionconst config = {
env: {
development: {
presets: [
[
'@babel/preset-env',
{
modules: false
}
]
]
},
production: {
presets: [
[
'@babel/preset-env',
{
modules: false
}
]
]
},
test: {
presets: [
[
'@babel/preset-env',
{
modules: 'commonjs'
}
]
],
plugins: [
'transform-es2015-modules-commonjs' // Not sure this is required, but I had added it anyway
]
}
}
};

Fixing Your Library Code for Jest

Configuring Jest to Recompile Libraries

// Jest configuration to recompile librariesconst path = require('path');const librariesToRecompile = [
'Library1',
'Library2'
].join('|');
const config = {
transformIgnorePatterns: [
`[\\\/]node_modules[\\\/](?!(${librariesToRecompile})).*$`
],
transform: {
'^.+\.jsx?$': path.resolve(__dirname, 'transformer.js')
}
};
// Babel-Jest Transformerconst babelJest = require('babel-jest');
const path = require('path');
const cwd = process.cwd();
const babelConfig = require(path.resolve(cwd, 'babel.config'));
module.exports = babelJest.createTransformer(babelConfig);

Npm/Yarn Link = The Devil

Library-Specific Optimizations

// MomenJS with locales stripped out by IgnorePluginconst { IgnorePlugin } from 'webpack';const config = {
plugins: [
new IgnorePlugin(/^\.\/locale$/, /moment/)
]
};
// MomentTimezone Webpack Pluginconst MomentTimezoneDataPlugin = require('moment-timezone-data-webpack-plugin');const config = {
plugins: [
new MomentTimezoneDataPlugin({
startYear: 2018,
endYear: 2100
})
]
};
// Babel Transform Imports// Babel configconst config = {
plugins: [
[
'transform-imports',
{
'lodash-es': {
transform: 'lodash/${member}',
preventFullImport: true
},
'react-bootstrap': {
transform: 'react-bootstrap/es/${member}', // The es folder contains es2015 module versions of the files
preventFullImport: true
}
}
]
]
};
// Full imports of these libraries are no longer allowed and will cause errorsimport _ from 'lodash-es';// Named imports are still allowedimport { debounce } from 'loash-es';// However, these named imports get compiled by Babel to look like this// import debounce from 'lodash-es/debounce';

Conclusion