Webpack 的DLL和TreeShaking

8/10/2021 webpack

# DLL

# 认识 DLL 库

  • DLL 全称是动态链接库(Dynamic Link Library),是为了软件在 Windows 中实现共享函数库的一种实现方式
  • webpack 中也有内置 DLL 的功能,它指的是我们可以将可以共享,并且不经常改变的代码,抽取成一个共享的库
  • 这个库在之后编译的过程中,会被引入到其它的项目的代码中

DLL 库的使用分为两步:

  • 第一步:打包一个 DLL 库
  • 第二步:项目中引入 DLL 库

注意:在升级到 webpack4 之后,React 和 Vue 脚手架都移除了 DLL 库,尤大认为 webpack4 已经能够提供很好的性能,不需要去专门维护一个 DLL 库了

# 打包一个 DLL 库

创建一个新的项目,例如:我们打包一个带有reactreact-dom的 DLL

webpack 帮助我们内置了一个 DllPlugin 可以帮助我们打包一个 DLL 的库文件

const path = require("path");
const webpack = require("webpack");

module.exports = {
  entry: {
    react: ["react", "react-dom"],
  },
  output: {
    path: path.resolve(__dirname, "./dll"),
    filename: "dll_[name].js",
    library: "dll_[name]",
  },
  plugins: [
    new webpack.DllPlugin({
      name: "dll_[name]",
      path: path.resolve(__dirname, "./dll/[name].manifest.json"), // 需要生成一个manifest文件,用来对应查找dll
    }),
  ],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

yarn build 以后就会生成一个 dll 文件夹,我们将打包后的 dll 文件运用到其他项目

# 使用打包的 DLL 库

如果我们在代码中使用到了 react 和 react-dom,我们有配置 splitChunks 的情况下,他们会进行分包,打包到一个独立的 chunk 中,但是我们有了 dll_react,就不再需要单独打包它们,可以直接取引用 dll_react

具体步骤如下:

  1. 通过 DllReferencePlugin 插件告知要使用的 DLL 库
  2. 通过 AddAssetHtmlPlugin 插件,将我们打包的 DLL 库引入到 Html 模块中
module.exports = {
  plugins: [
    new webpack.DllReferencePlugin({
      context: resolveApp("./"), // 告知在哪里查找dll
      manifest: resolveApp("./dll/react.manifest.json"), // 告知manifest的位置
    }),
    new AddAssetHtmlWebpackPlugin({
      // 该插件自动在html中注入dll_react引用,并在build中添加dll_react.js文件
      outputPath: "./auto",
      filepath: resolveApp("./dll/dll_react.js"),
    }),
  ],
};
1
2
3
4
5
6
7
8
9
10
11
12
13

# Terser

# Terser 介绍和安装

# 介绍

  • Terser是一个 JavaScript 的解释(Parser)、Mangler(绞肉机)/Compressor(压缩机)的工具集
  • 早期我们会使用uglify-js来压缩、丑化我们的 JS 代码,但是目前已经不再维护,并且不支持 ES6+的语法
  • Terser是从uglify-esfork 过来的,并且保留它原来的大部分 API 以及适配uglify-esuglify-js@3
  • 也就是说,Terser可以帮助我们压缩、丑化我们的代码,让我们的 bundle 变得更小

# 安装

因为Terser是一个独立的工具,所以它可以单独安装

# 全局安装
yarn global add terser
# npm install terser -g

# 局部安装
yarn add terser
# npm install terser
1
2
3
4
5
6
7

# 命令行使用 Terser

terser [input files] [options]
# 举例
# terser 输入文件 -o 输出文件名 -c(compress,后面可以配置参数,如:arrows=true,arguments=true) -m(mangle,同-c)
# 不传则使用默认配置,如果不配-c -m则只会压缩空格和回车
terser js/file1.js -o foo.min.js -c -m
terser js/file1.js -o foo.min.js -c arrows=true,arguments=true -m keep_classnames=true
1
2
3
4
5
6

关于 Terser 的详细配置可以查看文档compress option (opens new window)mangle option (opens new window)

# Compress 和 Mangle 的部分 options

# Compress option

  • arrows:class 或者 object 中的函数,转换成箭头函数
  • arguments:将函数中使用 arguments[index]装成对应的形参名称
  • dead_code:移除不可达的代码(tree shaking)
  • 其他属性可以查看文档

# Mangle option

  • toplevel:默认值是 false,顶层作用域中的变量名称,进行丑化(转换)
  • keep_classnames:默认值是 false,是否保持依赖的类名称
  • keep_fnames:默认值是 false,是否保持原来的函数名称
  • 其他属性可以查看文档
npx terser ./src/abc.js -o abc.min.js -c arrows,arguments=true,dead_code -m toplevel=true,keep_classnames=true,keep_fnames=true
1

# Terser 在 webpack 中配置

真实开发中,我们不需要手动的通过 terser 来处理我们的代码,我们可以直接通过 webpack 处理

  • 在 webpack 中有一个 minimizer 属性,在 production 模式下,默认就是使用TerserPlugin来处理我们的代码(当然还有其他的工具)
  • 如果我们对默认的配置不满意,也可以自己来创建 TerserPlugin 的实例,并且覆盖相关的配置

具体操作如下

  • 首先,我们需要打开 minimize,让其对我们的代码进行压缩(默认 production 模式下已经打开了)
  • 其次,我们可以在 minimizer 创建一个 TerserPlugin:
    • extractComments:默认值为 true,表示会将注释抽取到一个单独的文件中
      • 在开发中,我们不希望保留这个注释时,可以设置为 false
    • parallel:使用多进程并发运行提高构建速度,默认值是 true,并发运行的默认数量:os.cpus().length - 1(就是,cpus 的线程数-1)
      • 我们也可以设置自己的格式,但是使用默认值即可
    • terserOptions:设置我们的 terser 相关的配置
      • compress:设置压缩相关的选项
      • mangle:设置丑化相关的详细,可以直接设置为 true
      • toplevel:底层变量是否进行转换
      • keep_classnames:保留类的名称
      • keep_fnames:保留函数的名称
      • 其他相关参数可以查看上面的文档
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
  // ...
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,
        extractComments: false,
        terserOptions: {
          compress: {
            arguments: false,
            dead_code: true,
          },
          mangle: true,
          toplevel: true,
          keep_classnames: true,
          keep_fnames: true,
        },
      }),
    ],
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# CSS 的压缩

另一个代码的压缩是 css

  • css 压缩通常是去除无用的空格等,因为很难去修改选择器、属性的名称、值等
  • css 的压缩我们可以使用另外一个插件:css-minimizer-webpack-plugin
  • css-minimizer-webpack-plugin是使用 cssnano 工具来优化、压缩 CSS(也可以单独使用)

安装css-minimizer-webpack-plugin

yarn add css-minimizer-webpack-plugin -D
# npm install css-minimizer-webpack-plugin -D
1
2

在 optimization.minimizer 中配置

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({}),
      new CssMinimizerPlugin({
        parallel: true,
      }),
    ],
  },
};
1
2
3
4
5
6
7
8
9
10

# Scope Hoisting

Scope Hoisting 从 webpack3 开始增加的一个新功能,功能是对作用域进行提升,并且让 webpack 打包后的代码更小、运行更快

默认情况下 webpack 打包会有很多的函数作用域,包括一些(比如最外层的)IIFE

  • 无论是从最开始的代码运行,还是加载一个模块,都需要执行一系列的函数
  • Scope Hoisting 可以将函数合并到一个模块中来运行

使用 Scope Hoisting 非常的简单,webpack 已经内置了对应的模块

  • 在 production 模式下,默认这个模块就会启用
  • 在 development 模式下,我们需要自己来打开该模块
module.exports = {
  plugins: [new webpack.optimize.ModuleConcatenationPlugin()],
};
1
2
3

# Tree Shaking

# 认识 Tree Shaking

  • Tree Shaking 是一个术语,在计算机中表示消除死代码(dead_code)
  • 最早的想法起源于 LISP,用于消除未调用的代码(纯函数无副作用,可以放心的消除,这也是为什么要求我们再进行函数式编程时,尽量使用纯函数的原因之一)
  • 后来 Tree Shaking 也被应用于其他的语言,比如 JavaScript

javaScript 的 Tree Shaking

  • 对 JavaScript 进行 Tree Shanking 是源自打包工具 rollup
  • 这是因为 Tree shaking 依赖于 ES Module 的静态语法分析(不执行任何的代码,可以明确知道模块的依赖关系)
  • webpack2 正式内置支持了 ES2015 模块,和检测未使用模块的能力
  • 在 webpack4 正式扩展了这个能力,并且通过 package.json 的sideEffects属性作为标记,告知 webpack 在编译时,哪里文件可以安全的删除掉
  • webpack5 中,也提供了对部分 CommonJS 的 tree shaking 的支持,点击查看 (opens new window)

# webpack 实现 Tree Shaking

事实上 webpack 实现 Tree Shaking 采用了两种不同的方案

  • usedExports:通过标记某些函数是否被使用,之后通过 Terser 来进行优化的
  • sideEffects:跳过整个模块/文件,直接查看该文件是否有副作用

# usedExports

  • 将 mode 设置为 development 模式:
    • 为了可以看到 usedExports 带来的效果,我们暂时将环境设置为 development 模式
    • 因为在 production 模式下,webpack 默认的一些优化会带来很大的影响
  • 设置usedExports为 true 和 false 对比打包后的代码
    • 在 usedExports 设置为 true 时,会有一段注释:unused harmony export mul
    • 这段注释会告知 Terser,让其在优化时,可以删除掉这段代码
    • 我们会发现,在 minimize 设置为 true 时
      • sedExports 设置为 false 时,没有用到的函数没有被移除掉
      • 而 sedExports 设置为 true 时,没有用到的函数有被移除掉

所以usedExports实现 Tree shaking 是结合 Terser 来完成的,在 production 模式下,usedExports 是默认为 true 的,不需要我们去设置

# sideEffects

  • sideEffects 用于告知 webpack compiler 哪些模块是有副作用的
    • 副作用的意思是这里的代码有执行一些特殊的任务,不能仅仅通过 export 来判断这段代码的意义
  • 在 package.json 中设置 sideEffects 的值
    • 如果我们将 sideEffects 设置为 false,就是告知 webpack 可以安全的删除未用到的 exports
      • 例如我们有一个 format.js、style.css 文件
      • 该文件在导入时没有使用任何的变量来接受,那么打包后的文件,不会保留 format.js、style.css 相关的任何代码
    • 如果有一些我们希望保留,可以设置为数组
// package.json
{
  "sideEffects": ["./src/util/format.js", "*.css"]
}
1
2
3
4

一般情况下我们的模块都是没有副作用的,所以我们可以只对 css 文件进行处理

// package.json
{
  "sideEffects": false
}
1
2
3
4
// webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.css/.
        use: [],
        sideEffects: true // 设置这个属性就不会删除css文件
      }
    ]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

建议

如何在项目中对 JavaScript 的代码进行 TreeShaking 呢(生成环境)?

  • 在 optimization 中配置 usedExports 为 true,来帮助 Terser 进行优化;
  • 在 package.json 中配置 sideEffects,直接对模块进行优化;

# CSS 实现 Tree Shaking

CSS 的 Tree Shaking 需要借助一些其他的插件

  • 早期的时候,我们会使用 PurifyCss 插件来完成 Css 的 Tree Shaking,但是目前改库已经不再维护了(最新更新也是 4 年前了)
  • 目前我们可以使用PurgeCSS来完成 Css 的 Tree Shaking,也就是帮助我们删除未使用的 CSS 工具

安装 PurgeCss 的 webpack 插件

yarn add purgecss-webpack-plugin -D
# npm install purgecss-webpack-plugin -D
1
2

配置 PurgeCss,一般在生产环境

  • paths: 表示要检测哪些目录下的内容需要被分析,这里我们可以使用 glob
  • 默认情况下,Purgecss 会将我们的 html 标签的样式移除掉,如果我们希望保留,可以添加一个 safelist 的属性
module.exports = {
  // ...
  plugins: [
    new PurgecssPlugin({
      paths: glob.sync(`${resolveAp*p("./src")}/**/*`, { nodir: true })
          safelist: function() {
            return {
              standard: ["body", "html"]
            }
          }
    })
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  • purgecss 也可以对 less 文件进行处理(所以它是对打包后的 css 进行 tree shaking 操作)

# 其他

# HTTP 压缩

HTTP 压缩是一种内置在服务器和客户端之间的,以改进传输速度和带宽利用率的方式

HTTP 压缩的流程是

  • 第一步:HTTP 数据在服务器发送钱就已经被压缩了(可以在 webpack 中完成)
  • 第二步:兼容的浏览器在向服务器发送请求时,会告知服务器自己支持哪些压缩格式
GET /encrypted-area HTTP/1.1
Host: www.exaple.com
Accpet-Encoding: gip, deflate
1
2
3
  • 第三步:服务器在浏览器支持的压缩格式下,直接返回对应的压缩后的文件,并且在响应头中告知浏览器
HTTP/1.1 200 OK
Date: Tue, 10 Aug 2021 06:03:16 GMT
Last-Modified: wed, 08 Jan 2021 23:11:55 GMT
Accept-Ranges: bytes
Content-Length: 438
Connect-Type: text/html; charset=UTF-8
Content-Encoding: gzip
1
2
3
4
5
6
7

目前的压缩合适非常的多

  • compress:UNIX 的"compress"程序的方法(历史性原因,不推荐大多数应用使用,应该使用 gip 或者 deflate)
  • deflate:基于 deflate 算法(定义与 RFC 1951)的压缩,使用 zlib 数据格式封装
  • gzip:GUN zip 格式(定义与 RFC 1952),是目前使用比较广泛的压缩算法
  • br:一种新的开源压缩算法,专为 HTTP 内容的编码而设计

Webpack 实现对文件的压缩

webpack 中相当于是实现了 HTTP 压缩的第一步操作,我们可以使用 CompressionPlugin

第一步,安装 CompressionWebpackPlugin

yarn add compression-webpack-plugin -D
# npm install compression-webpack-plugin -D
1
2
  1. 使用 CompressionWebpackPlugin 即可
new CompressionPlugin()({
  test: /\.(css|js)$/, // 匹配哪些文件需要压缩
  // threshold: 500, // 设置文件从多大开始压缩
  minRatio: 0.7, // 至少的压缩比例
  algorithm: "gzip", // 采用的压缩算法
});
1
2
3
4
5
6

# HTML 文件中代码的压缩

我们之前使用了 HtmlWebpackPlugin 插件来生成 HTML 的模板,事实上它还有一些其他的配置

  • inject:设置打包的资源插入的位置
    • true、false:表示插入或不插入
    • body、head:表示插入到 body 或者 head 中
  • cache:设置为 true,只有当文件改变时,才会生成新的文件(默认值是 true)
  • minify:默认会使用一个插件html-minifier-terser
new HtmlWebpackPlugin({
  inject: "body",
  cache: true,
  minify: isProduction
    ? {
        removeComments: true, // 移除注释
        collapseWhitespace: false, // 折叠空格
        removeRedundantAttributes: false, // 移除多余的属性 例如type=text
        useShortDoctype: true, // 比如我们的模板是html4,那么会转换成html5的文档
        removeStyleLinkTypeAttributes: true, // 比如link中的type="text/css"
        keepClosingSlash: true, // 是否保存单元素的尾部
        minifyCSS: false, // 是否压缩css
        minifyJS: {
          mangle: {
            toplevel: true,
          },
        },
      }
    : false,
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# InlineChunkHtmlPlugin

InlineChunkHtmlPlugin 可以辅助将一些 chunk 出来的模块,内联到 html 中,例如:runtime 的代码,代码量不大,但是必须加装的,那么我们可以内联到 html 中

这个插件是在`reac-dev-utils1 中实现的,所以我们可以安装它

yarn add react-dev-utils -D
# npm install react-dev-utils -D
1
2

在生成环境的 plugins 中进行配置

在 production 的 plugins 中进行配置

const InlineChunkHtmlPlugin = require("react-dev-utils/InlineChunkHtmlPlugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  // ...
  plugins: [
    // ...
    new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime.+\.js/]),
  ],
};
1
2
3
4
5
6
7
8
9
10
Last Updated: 1/21/2025, 10:16:53 AM