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?

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

  • 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 👏