How to 🌲 shake
Optimizing JavaScript libraries for Webpack
What we’ll cover
- What is tree shaking and why it matters
- CommonJS and ES Modules
- Trade offs in bundling strategies
- Side effect gotchas
Why am I talking about this?
- Investigated Search Engine Listing Manager client bundle size (was large)
- Stencil (Digital's UI component library) was part of the problem
- Stencil 2.0.2 now has support for tree shaking
What is tree shaking?
- Dead code elimination
- Webpack v2 added support for tree shaking when authors use static ES modules (import + export)
- Webpack v4 expanded support for tree shaking dependencies
- This is done by providing hints to Webpack via package.json
- https://webpack.js.org/guides/tree-shaking/
Let's build something.
Goal: Build a math library that...
- Can be used by both Node and the browser
- Eliminates any unused code in the browser environment
import { add, subtract } from 'math-lib';
let result = add(2, 3);
result = subtract(result, 4);
Interlude: Let's talk about module systems
Two most common module systems in JavaScript
-
CommonJS
-
ECMA Script Modules (ES Modules)
CommonJS*
// In circle.js
const PI = Math.PI;
exports.area = (r) => PI * r * r; exports.circumference = (r) => 2 * PI * r;
// In some file
const circle = require('./circle.js');
console.log( `The area of a circle of radius 4 is ${circle.area(4)}`);
*Technically this is Node's take on CommonJS
CommonJS
- Designed with server development in mind
- require() can be called anywhere
- Synchronous API makes it not suitable for client-side
- Hard to analyze for static code analyzers
ES Modules
//------ lib.js ------ export const sqrt = Math.sqrt; export function square(x) { return x * x; } export function diag(x, y) { return sqrt(square(x) + square(y)); } //------ main.js ------ import { square, diag } from 'lib'; console.log(square(11)); // 121 console.log(diag(4, 3)); // 5
ES Modules
- Support for static analysis tools
- Synchronous and asynchronous loading supported*
- Supported in modern browsers (no IE11)
- Only supported in Node JS 8.5+ with *.mjs file extension and ---experimental-modules runtime flag
Async import is Stage 3
Let's recap
Goal: Build a library
- Can be used by both Node and the browser
- Need to support CommonJS for most Node environments
- Eliminates any unused code in the browser environment
- Need to support ES Modules for static code analysis necessary for tree shaking
math-lib
Part 1
CommonJS
- The library doesn't require any build system
- The library works out of the box with Node
- The browser gets all code including unused modules ☹️
What we learned
Part 2
CommonJS "Optimized"
- When using CommonJS, requiring in the direct module can help optimize client-side bundles
- This is why you should import lodash modules directly
import get from 'lodash/get';
What we learned
Part 3
Bundling ES Modules with Webpack
- While Webpack is great as an application bundler, it isn't optimized for libraries
- Yes, Rollup would be better in this case
What we learned
Part 4
Convert ES to CJS Modules without bundling
- Webpack as an application bundler supports multiple ways of loading modules as specified in a library's package.json
- "main" - meant for Node environments
- "module" - optimized for ES Module supported environments
- Don't bundle libraries with Webpack; instead transpile (with Babel)
What we learned
Part 5
Tree shaking classes?
- Classes are not well suited for tree shaking
- If you're worried about the impact of your library on bundle size (and you probably should be), use functions over classes
What we learned
Part 6
Side effects
- Static properties on classes or functions can cause side effects
- Anything that may cause a side effect will automatically be included in your bundle even if you don't explicitly import it
What we learned
Note: This is where Rollup failed me in my own attempts at optimizing Stencil.
Part 7
Handling side effects
- Webpack can be told to ignore potential side effects by setting the flag "sideEffects" to false in the library's package.json
What we learned
- CommonJS modules cannot be tree shaken
- Don't bundle your library with Webpack
- Libraries should support both ES and CommonJS syntax
- Specify your library's CommonJS entry point via "main"
- Specify your library's ES Module entry point via "module"
- Functions optimize better than classes
- Side effect (e.g. static properties) can be side stepped by setting "sideEffects" to false in library's package.json
- Look at your app or library bundles every now and then to verify what's in there is what you expect
Summary
Thanks 👏
How to make your code tree shake
By Eric Masiello
How to make your code tree shake
- 606