为什么 webpack 在使用 "import * as _" 时不对 lodash 进行 tree-shaking?

Why webpack doesn't tree-shake the lodash when using "import * as _"?

我正在学习使用 Lodash 的 webpack 4/React 应用程序进行 tree-shaking。

起初,我的 Lodash 用法是这样的:

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

我很快通过 BundleAnalyzerPlugin 了解到整个 Lodash 都包含在开发和生产版本 (527MB) 中。

googling around 之后我意识到我需要使用特定的语法:

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

现在,只有 random 和它的依赖项被正确地包含在包中,但我仍然有点困惑。

如果我需要在我的 import 语句中明确指定函数,那么 tree-shaking 实际上扮演了什么角色? 比较开发模式构建和生产模式构建时,BundleAnalyzerPlugin 没有显示有效载荷大小的差异(两者都是正确的小尺寸,但我认为 tree-shaking 只发生在生产构建中?)。

我的印象是 TreeShaking 会执行某种静态代码分析以确定代码的哪些部分实际被使用(可能基于功能?)并剪掉未使用的位。

为什么我们不能总是在我们的 import 中使用 * 并依靠 TreeShaking 来确定捆绑中实际包含的内容?

如果有帮助,这是我的 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")
    }
  }
};

我正在使用 webpack --mode=developmentwebpack --mode=production 调用 webpack。

其实和webpack的tree-shaking能力没有关系。基于关于 tree-shaking

的 Webpack 文档

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

当您根据链接文档在 package.json 上设置 "sideEffects: false 时:

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.

如果你有一些你知道它们是纯粹的文件或包,将它们添加到 sideEffects 以在未使用时修剪它。还有一些其他解决方案可以做 tree-shaking,我建议阅读 Webpack 文档上的整个 article

其中一种手动方式是使用直接导入,如下所示:

import get from 'lodash/get';

Webpack 理解 add 只是从整个 lodash 包中获取。另一种方法是破坏导入,需要一些 Webpack optimization 来进行 tree-shaking,所以你应该像下面这样导入:

import { get } from 'lodash';

此外,另一种棘手的方法是安装特定的软件包,我的意思是:

yarn add lodash.get

npm install --save lodash.get

然后导入只需写:

import get from 'lodash.get';

当然,这不是tree-shaking,它是一种紧缩的心态发展,但它导致你只是添加你想要的。

但是

不要做任何上述解决方案,你只需通过编写 import * as _ from "lodash"; 添加整个包,然后使用 _.random 或任何函数和期望 Webpack 明白你想要摇树的发生吗?

当然,Webpack 运行良好。你应该使用一些配置和编码风格来查看 tree-shaking 的发生。

所有两个现有答案都是错误的,webpack 做 treeshake import *,但是只有当您使用 esmodule 时才会发生这种情况,而 lodash 则不是。正确的解决方法是使用lodash-es

编辑:这个答案只适用于 webpack4,而 webpack 5 支持 commonjs 的 tree shaking 的有限子集,但我自己没有测试过

如果您已经在使用 Babel,正确使用 tree shake lodash 的最简单方法是使用 official babel-plugin-lodash by the lodash team.

这使用 Babel 将您的 lodash 导入重写为更可摇动树的形式。这样做使我的团队的包大小减少了 ~32kB(压缩)不到 5 分钟的努力。