23

I am learning about tree-shaking with a webpack 4/React application that uses Lodash.

At first, my Lodash usage looked like this:

import * as _ from "lodash";
_.random(...

I soon learned, via the BundleAnalyzerPlugin, that the entirety of Lodash was being included in both dev and prod builds (527MB).

After googling around I realized that I needed to use a specific syntax:

import random from "lodash/random";
random(...

Now, only random and it's dependencies are correctly included in the bundle, but I'm still a little confused.

If I need to explicitly specify functions in my import statement, then what role is the tree-shaking actually playing? The BundleAnalyzerPlugin isn't showing a difference in payload size when comparing between dev and production mode builds (it's the correct small size in both, but I thought that tree-shaking only took place with production builds?).

I was under the impression that TreeShaking would perform some sort of static code analysis to determine which parts of the code were actually being used (perhaps based on function?) and clip off the unused bits.

Why can't we always just use * in our import and rely on TreeShaking to figure out what to actually include in the bundle?

In case it helps, here is my webpack.config.js:

const path = require("path");
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;

module.exports = {
  entry: {
    app: ["babel-polyfill", "./src/index.js"]
  },
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: "static",
      openAnalyzer: false
    })
  ],
  devtool: "source-map",
  output: {
    filename: "[name].js",
    path: path.resolve(__dirname, "dist"),
    chunkFilename: "[name].bundle.js",
    publicPath: ""
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: "babel-loader",
        include: /src/,
        options: {
          babelrc: false,
          presets: [
            [
              "env",
              {
                targets: {
                  browsers: ["last 2 Chrome versions"]
                }
              }
            ],
            "@babel/preset-env",
            "@babel/preset-react"
          ],
          plugins: ["syntax-dynamic-import"]
        }
      },
      {
        test: /\.(ts|tsx)$/,
        use: [
          {
            loader: require.resolve("ts-loader"),
            options: {
              compiler: require.resolve("typescript")
            }
          }
        ]
      }
    ]
  },
  resolve: {
    symlinks: false,
    extensions: [".js", ".ts", ".tsx"],
    alias: {
      react: path.resolve("./node_modules/react")
    }
  }
};

I'm invoking webpack with webpack --mode=development and webpack --mode=production.

AmerllicA
  • 29,059
  • 15
  • 130
  • 154
Jonathan.Brink
  • 23,757
  • 20
  • 73
  • 115
  • It [really should](https://stackoverflow.com/a/45746950/1048572) [not matter](https://stackoverflow.com/a/50767431/1048572). What exactly are you doing with the `_` object, more than just calling methods on it? – Bergi Nov 07 '19 at 02:59
  • Did you mean `import random from "lodash/random";`? `import * as _ from "lodash";` should tree-shake exactly the same as `import { random } from "lodash";`. – Bergi Nov 07 '19 at 03:01
  • @Bergi, yes, sorry, I meant "lodash/random", which I had correctly in my code, just typed it wrong in the question. – Jonathan.Brink Nov 07 '19 at 14:16
  • @Bergi, thanks for the link, but when I use the "import * as _" syntax the bundle has the entire lodash library, so it seems like it does matter... – Jonathan.Brink Nov 07 '19 at 14:20
  • I think this is because lodash is not tree-shakeable (bad design). The `import { random } from "lodash";` should not work either, right? – Bergi Nov 07 '19 at 14:34
  • When I use `import { random } from "lodash/random";` it does indeed "tree-shake" so that the bundle only includes `random` and not all of lodash. But I'm confused...am I actually taking advantage of the tree-shaking feature when I write my import in this manner? – Jonathan.Brink Nov 07 '19 at 14:38
  • Not really, no. `lodash` is not tree-shakeable. That's why you have to manually import sub-modules. – Bergi Nov 07 '19 at 14:52
  • I suppose that's also my understanding, it's just that it's hard to find official sources that explicitly state that "lodash is not tree-shakable" which seems to indeed be the case – Jonathan.Brink Nov 07 '19 at 15:13

3 Answers3

28

All two existing answers are wrong, webpack do treeshake import *, however that only happens when you're using a esmodule, while lodash is not. The correct solution is to use lodash-es

Edit: this answer only applies to webpack4, while webpack 5 supported a limited subset of tree shaking for commonjs, but I haven't tested it myself

Austaras
  • 901
  • 8
  • 24
  • 1
    `lodash-es` has [maintenance issues](https://github.com/lodash/lodash/issues/4879) and any attempt to use it requires aliasing `lodash` as `lodash-es` in your bundler of choice or you'll get `lodash-es` for your project and `lodash` for some dependencies which choose to use `lodash/*` imports. Moreover, even incorrect `lodash` imports can be fixed with the [`transform-imports`](https://github.com/lodash/lodash/issues/4879) Babel plugin. – zamber Sep 30 '20 at 21:14
  • seems like it's alredy been fixed – Austaras Jan 18 '21 at 03:43
  • 1
    With `sideEffects:false` in `package.json` and `mode:production` in the `mocha.config.js`, it still isn't tree shaking when `lodash-es` is installed and `lodash` is uninstalled. However using `import join from 'lodash/join' sends the bundle size from 77k to <1K. – Craig Hicks Jun 02 '21 at 00:16
  • @CraigHicks you should import from `lodash-es` not `lodash` – Austaras Jun 02 '21 at 09:28
  • @Austaras - You are right! I'd omitted the additional step `npm i -D @types/lodash-es`. – Craig Hicks Jun 02 '21 at 14:49
6

Actually, it is not related to the Webpack ability to tree-shake. base on Webpack docs about tree-shaking

The new webpack 4 release expands on this capability with a way to provide hints to the compiler via the "sideEffects" package.json property to denote which files in your project are "pure" and therefore safe to prune if unused

When you set the "sideEffects: false on your package.json based on the linked docs:

All the code noted above does not contain side effects, so we can simply mark the property as false to inform webpack that it can safely prune unused exports.

If you have some files or packages which you know they are pure add them to sideEffects to prune it if unused. There are some other solutions to do tree-shaking that I proffer to read the whole article on Webpack docs.

One of the manual ways are using direct importing like below:

import get from 'lodash/get';

That Webpack understands add just get from the whole lodash package. another way is destructing importing that needs some Webpack optimization for to tree-shaking, so you should import like below:

import { get } from 'lodash';

Also, another tricky way is just to install a specific package, I mean:

yarn add lodash.get

OR

npm install --save lodash.get

Then for import just write:

import get from 'lodash.get';

Definitely, it is not tree-shaking, it is a tight mindset development, but it causes you just add what you want.

BUT

YOU DON'T DO ANYTHING OF ABOVE SOLUTIONS, you just add the whole package by writing import * as _ from "lodash"; and then use _.random or any function and expect Webpack understand you wanna the tree-shaking be happening?

Surely, the Webpack works well. you should use some configs and coding style to see the tree-shaking happens.

AmerllicA
  • 29,059
  • 15
  • 130
  • 154
  • "If you have some files or packages which you know they are pure add them to sideEffects to prune it if unused. " ---- Your written message has inverted the sense. You should add them to sideEffects if you know they are NOT pure. That is what your reference says. – Craig Hicks Jun 01 '21 at 23:48
  • @CraigHicks, Ok what does it mean? or What should I do? – AmerllicA Jun 02 '21 at 16:27
  • Actually it is not your fault - the webpack description at the top is misleading. Go further down though to the example here (https://webpack.js.org/guides/tree-shaking/#mark-the-file-as-side-effect-free) and you will see that `sideEffects:false` means no files have side effects, and files with side effects should listed individually, e.g. `sideEffects:['./fileWithSideEffects.js','./anotherFileWithSideEffects.js']`. I think the default is `sideEffects:false`, because tree shaking worked without setting `sideEffects` at all. (I did use `lodash-es` and `@types/lodash-es`). – Craig Hicks Jun 02 '21 at 19:20
  • @CraigHicks, Thanks for your informing. I will send an upvote to prize you for your effort and explanation. thanks. – AmerllicA Jun 03 '21 at 11:02
3

If you're already using Babel, the easiest method to properly tree shake lodash is to use the official babel-plugin-lodash by the lodash team.

This uses Babel to rewrite your lodash imports into a more tree-shakeable form. Doing this dropped my team's bundle size by ~32kB (compressed) with less than 5 minutes of effort.

zeptonaut
  • 895
  • 3
  • 10
  • 20