2023-01-14
前端工程化
00

目录

前言
1. 代码分离 Code Spliting
1.1 方式一:多入口起点
1.1.1 没有代码分离时
1.1.2 有代码分离时
1.1.3 优化:移除重复的模块
1.2 方式二:splitChunks 模式
1.2.1 splitChunk 的配置
1.2.2 SplitChunks 自定义配置解析
1.3 方式三:动态导入(dynamic import)
1.3.1 import 方式
1.3.2 动态导入的文件命名
1.4 CDN 加速
1.4.1 配置自己的 CDN 服务器
1.4.2 配置第三方库的CDN服务器
1.5 补充
1.5.1 解决注释的单独提取
1.5.2 chunkIds 的生成方式
1.5.3. runtimeChunk 的配置
1.5.4. Prefetch 和 Preload
2. Shimming 预制依赖
2.1 Shimming 预支全局变量
3. TerserPlugin 代码压缩
3.1 Terser 介绍
3.2 Terser 在 webpack 中配置(JS 的压缩)
3.3 CSS 的压缩
4. Tree Shaking
4.1 webpack 实现 Tree Shaking
4.1.1 usedExports
4.1.2 sideEffects
4.2 CSS 实现 Tree Shaking
4.3 Scope Hoisting
5. webpack 对文件压缩
6. HTML 文件中代码的压缩

前言

webpack 作为前端目前使用最广泛的打包工具,在面试中也是经常会被问到的。

比较常见的面试题包括:

  • 可以配置哪些属性来进行 webpack 性能优化?
  • 前端有哪些常见的性能优化?(除了其他常见的,也完全可以从 webpack 来回答)

webpack 的性能优化比较多,我们可以对其进行分类:

  1. 打包后的结果,上线时的性能优化。(比如分包处理、减小包体积、CDN服务器等)
  2. 优化打包速度,开发或者构建时优化打包速度。(比如 excludecache-loader 等)

大多数情况下,我们会更加侧重于 第一种,因为这对线上的产品影响更大。

虽然在大多数情况下,webpack 都帮我们做好了该有的性能优化:

  • 比如配置 modeproduction 或者 development 时,默认 webpack 的配置信息;
  • 但是我们也可以针对性的进行自己的项目优化;

本章,就让我们来学习一下 webpack 性能优化的更多细节

1. 代码分离 Code Spliting

代码分离(Code Spliting)webpack 一个非常重要的特性,它主要的目的是将代码剥离到不同的 bundle 中,之后我们可以按需加载,或者并行加载这些文件

什么意思呢?举个例子:

  • 没有使用代码分离 时:
    1. webpack 将项目中的所有代码都打包到 一个 index.js 文件中(假如这个文件有 10M
    2. 当我们在生产环境去访问页面时,浏览器必须得将这 10Mindex.js 文件全部下载解析执行后页面才会开始渲染。
    3. 假如此时的网速是 10M/s,那么光是去下载这个 index.js 文件会花去 1s 。(这 1s 中内页面是白屏的)
    4. 在改动了部分代码第二次打包后,因为是全新的文件,浏览器又要重新下载一次
  • 使用代码分离 时:
    1. webpack 将项目中的所有代码都打包到是 多个 js 文件中(我们假设每个文件都为 1M
    2. 当我们在生产环境去访问页面时,此时浏览器将 1Mindex.js 文件下载就只需要 0.1s 了,至于其它的文件,可以选择需要用到它们时候加载或者和 index.js 文件并行的下载
    3. 在改动了部分代码第二次打包后,浏览器可以值下载改动过的代码文件,对于没改动过的文件可以直接从缓存中拿去。

通过以上的例子,相信大家应该能理解 代码分离 的好处了,那么在 webpack 如何能实现代码分离呢?

webpack 常用的代码分离方式有三种

  1. 入口起点:使用 entry 配置手动分离代码;
  2. 防止重复:使用 EntryDependencies 或者 SplitChunksPlugin 去重和分离代码:
  3. 动态导入:通过模块的内联函数用来分离代码

1.1 方式一:多入口起点

这是迄今为止最简单直观的分离代码的方式。不过,这种方式手动配置较多,并有一些隐患,我们将会解决这些问题。

先来看看如何从 main bundle 中分离 another module(另一个模块)

1.1.1 没有代码分离时

创建一个小的 demo

  1. 首先我们创建一个目录,初始化 npm,然后在本地安装 webpackwebpack-cliloadsh
shell
mkdir webpack-demo cd webpack-demo npm init -y npm install webpack webpack-cli lodash --save-dev
  1. 创建 src/index.js
js
import _ from "lodash"; console.log(_);
  1. 创建 src/another-module.js
js
import _ from 'lodash'; console.log(_);
  1. 创建 webpack.config.js
const path = require("path"); module.exports = { mode: "development", entry: "./src/index.js", output: { path: path.resolve(__dirname, "dist"), filename: "main.js", }, };
  1. package.json 中添加命令:
json
"scripts": { "build": "webpack" },
  1. 执行命令进行打包:
shell
npm run build
  1. 生成如下构建结果:

image.png

可以看到此时生成了一个 554KBmain.js 文件

1.1.2 有代码分离时

接下来我们从 main bundle 中分离出 another module(另一个模块)

  1. 修改 webpack.config.js
diff
const path = require("path"); module.exports = { mode: "development", - entry: './src/index', + entry: { + index: './src/index', + another: './src/another-module.js' + }, output: { path: path.resolve(__dirname, "dist"), - filename: "main.js", + filename: "[name].main.js", }, };
  1. 打包,生成如下构建结果:

image.png

我们发现此时已经成功打包出 another.bundle.jsindex.bundle.js 两个文件了,但是文件的大小似乎有些问题,怎么两个都是 554KB

正如前面提到的,这种方式存在一些隐患:

  1. 如果入口 chunk 之间包含一些重复的模块,那些重复模块都会被引入到各个 bundle 中。
  2. 这种方法不够灵活,并且不能动态地将核心应用程序逻辑中的代码拆分出来。

以上两点中,第一点对我们的示例来说无疑是个问题,因为之前我们在 ./src/index.js 中也引入过 lodash,这样就在两个 bundle 中造成重复引用。在下一小节我们将移除重复的模块。

1.1.3 优化:移除重复的模块

在通过多入口分离代码的方式中,我们可以通过配置 dependOn 这个选项来解决重复模块的问题,它的原理就是从两个文件中抽出一个共享的模块,然后再让这两个模块依赖这个共享模块。

  1. 修改 webpack.config.js 配置文件:
diff
const path = require('path'); module.exports = { mode: 'development', entry: { - index: './src/index.js', - another: './src/another-module.js', + index: { + import: './src/index.js', + dependOn: 'shared', + }, + another: { + import: './src/another-module.js', + dependOn: 'shared', + }, + shared: ['lodash'], }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), }, };
  1. 打包,生成如下构建结果:

image.png

可以看到 index.mian.jsanother.mian.js 中重复引用的部分被抽离成了 shared.main.js 文件,且 index.mian.jsanother.mian.js 文件大小也变小了。

1.2 方式二:splitChunks 模式

另外一种分包的模式是 splitChunks,它底层是使用 SplitChunksPlugin 来实现的:

SplitChunksPlugin 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk

因为该插件 webpack 已经默认安装和集成,所以我们并 不需要单独安装和直接使用该插件;只需要提供 SplitChunksPlugin 相关的配置信息即可

webpack 提供了 SplitChunksPlugin 默认的配置,我们也可以手动来修改它的配置:

  • 比如默认配置中,chunks 仅仅针对于异步(async)请求,我们可以设置为 initial 或者 all

1.2.1 splitChunk 的配置

  1. 1.1.2 的基础上修改 webpack.cofig.js
diff
const path = require('path'); module.exports = { mode: 'development', entry: { index: './src/index.js', another: './src/another-module.js', }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), }, + optimization: { + splitChunks: { + chunks: 'all', + }, + }, };
  1. 打包,生成如下构建结果:

image.png

使用 optimization.splitChunks 配置选项之后,现在应该可以看出,index.bundle.jsanother.bundle.js 中已经移除了重复的依赖模块。需要注意的是,插件将 lodash 分离到单独的 chunk,并且将其从 main bundle 中移除,减轻了大小。

除了 webpack 默认继承的 SplitChunksPlugin 插件,社区中也有提供一些对于代码分离很有帮助的 pluginloader,比如:

1.2.2 SplitChunks 自定义配置解析

关于 optimization.splitChunks 文档上有很详细的记载,我这里讲你叫几个常用的:

1. Chunks:

  • 默认值是 async
  • 另一个值是 initial,表示对通过的代码进行处理
  • all 表示对同步和异步代码都进行处理

2. minSize

  • 拆分包的大小, 至少为 `minSize;
  • 如果一个包拆分出来达不到 minSize ,那么这个包就不会拆分;

3. maxSize

  • 将大于maxSize的包,拆分为不小于minSize的包;

4. cacheGroups:

  • 用于对拆分的包就行分组,比如一个 lodash 在拆分之后,并不会立即打包,而是会等到有没有其他符合规则的包一起来打包;
  • test 属性:匹配符合规则的包;
  • name 属性:拆分包的 name 属性;
  • filename 属性:拆分包的名称,可以自己使用 placeholder 属性;
  1. 修改 webpack.config.js
js
const path = require("path"); module.exports = { mode: "development", entry: { index: "./src/index.js", another: "./src/another-module.js", }, output: { filename: "[name].bundle.js", path: path.resolve(__dirname, "dist"), }, optimization: { splitChunks: { chunks: "all", // 拆分包的最小体积 // 如果一个包拆分出来达不到 minSize,那么这个包就不会拆分(会被合并到其他包中) minSize: 100, // 将大于 maxSize 的包,拆分成不小于 minSize 的包 maxSize: 10000, // 自己对需要拆包的内容进行分组 cacheGroups: { 自定义模块的name: { test: /node_modules/, filename: "[name]_vendors.js", }, }, }, }, };
  1. 打包,生成如下构建结果:

image.png

1.3 方式三:动态导入(dynamic import)

另外一个代码拆分的方式是动态导入时,webpack 提供了两种实现动态导入的方式:

  • 第一种,使用 ECMAScript 中的 import() 语法来完成,也是目前推荐的方式;
  • 第二种,使用 webpack 遗留的 require.ensure,目前已经不推荐使用;

动态 import 使用最多的一个场景是懒加载(比如路由懒加载)

1.3.1 import 方式

接着从 1.1.2 小节代码的基础上修改:

  1. 修改 webpack.confg.js
js
const path = require("path"); module.exports = { entry: "./src/index.js", mode: "development", entry: { index: "./src/index.js", }, output: { filename: "[name].bundle.js", path: path.resolve(__dirname, "dist"), }, };
  1. 删除 src/another-module.js 文件

  2. 修改 src/index.js,不再使用 statically import (静态导入) lodash,而是通过 dynamic import(动态导入) 来分离出一个 chunk

js
const logLodash = function () { import("lodash").then(({ default: _ }) => { console.log(_); }); }; logLodash();

之所以需要 default,是因为 webpack 4 在导入 CommonJS 模块时,将不再解析为 module.exports 的值,而是为 CommonJS 模块创建一个 artificial namespace 对象。

  1. 打包,生成如下构建结果:

image.png

由于 import() 会返回一个 promise,因此它可以和 async 函数一起使用。下面是如何通过 async 函数简化代码:

js
const logLodash = async function () { const { default: _ } = await import("lodash"); console.log(_); }; logLodash();

1.3.2 动态导入的文件命名

因为动态导入通常是一定会打包成独立的文件的,所以并不会再 cacheGroups 中进行配置;

它的命名我们通常会在 output 中,通过 chunkFilename 属性来命名:

  1. 修改 webpack.config.js
diff
const path = require("path"); module.exports = { entry: "./src/index.js", mode: "development", entry: { index: "./src/index.js", }, output: { filename: "[name].bundle.js", path: path.resolve(__dirname, "dist"), + chunkFilename: "chunk_[name].js"" }, };
  1. 打包构建:

image.png

如果对打包后的 [name] 不满意,还可以通过 magic comments(魔法注释)来修改:

1, 修改 src/index.js

js
const logLodash = async function () { const { default: _ } = await import(/*webpackChunkName: 'lodash'*/ "lodash"); console.log(_); }; logLodash();
  1. 打包构建

image.png

1.4 CDN 加速

CDN 称之为 内容分发网络Content Delivery Network 或 Content Distribution Network,缩写:CDN

  • 它是指通过相互连接的网络系统,利用最靠近每个用户的服务器;
  • 更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户;
  • 来提供高性能、可扩展性及低成本的网络内容传递给用户;

image.png

在开发中,我们使用 CDN 主要是两种方式:

  • 方式一:打包的所有静态资源,放到 CDN 服务器,用户所有资源都是通过 CDN 服务器加载的;
  • 方式二:一些第三方资源放到 CDN 服务器上;

1.4.1 配置自己的 CDN 服务器

如果所有的静态资源都想要放到 CDN 服务器上,我们需要购买自己的 CDN 服务器;

  • 目前阿里、腾讯、亚马逊、Google 等都可以购买 CDN 服务器;
  • 我们可以直接修改 publicPath,在打包时添加上自己的 CDN 地址;
  1. 1.3.1 的基础上安装 HtmlWebpackPlugin 插件:
shell
npm install --save-dev html-webpack-plugin
  1. 修改 webpack.config.js 文件:
diff
const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { entry: "./src/index.js", mode: "development", entry: { index: "./src/index.js", }, output: { filename: "[name].bundle.js", path: path.resolve(__dirname, "dist"), chunkFilename: "chunk_[name].js", + publicPath: "https://yejiwei.com/cdn/", }, plugins: [new HtmlWebpackPlugin()], };
  1. 打包构建

image.png

可以发现我们打包后的 script 标签自动添加了 CDN 服务器地址的前缀。

1.4.2 配置第三方库的CDN服务器

通常一些比较出名的开源框架都会将打包后的源码放到一些比较出名的、免费的 CDN 服务器上:

  • 国际上使用比较多的是 unpkgJSDelivrcdnjs
  • 国内也有一个比较好用的 CDNbootcdn

在项目中,我们如何去引入这些 CDN 呢?

  • 第一,在打包的时候我们不再需要对类似于 lodash 或者 dayjs 这些库进行打包;
  • 第二,在 html 模块中,我们需要自己加入对应的 CDN 服务器地址;
  1. 创建 public/index.html 模版,手动加上对应 CDN 服务器地址
html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> <script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.core.min.js"></script> </head> <body></body> </html>
  1. 1.3.1 的基础上修改 webpack.config.js配置,来排除一些库的打包并配置 html 模版:
diff
const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { entry: "./src/index.js", mode: "development", entry: { index: "./src/index.js", }, output: { filename: "[name].bundle.js", path: path.resolve(__dirname, "dist"), chunkFilename: "chunk_[name].js", }, plugins: [ new HtmlWebpackPlugin({ + template: "./public/index.html", }), ], + externals: { + lodash: "_", + }, };
  1. 打包构建

image.png

image.png

1.5 补充

以下补充了解即可(一些细节)

1.5.1 解决注释的单独提取

如果将 webpack.config.jsmode 改为 production 也就是生产环境时,经常会看到一写 .txt 后缀的注释文件

image.png

这是因为在 production 默认情况下,webpack 再进行分包时,有对包中的注释进行单独提取。

这个包提取是由另一个插件(TerserPlugin 后面会细说) 默认配置的原因,如果想去掉可以做以下配置:

image.png

1.5.2 chunkIds 的生成方式

optimization.chunkIds 配置用于告知 webpack 模块的 id 采用什么算法生成。

有三个比较常见的值:

  • natural:按照数字的顺序使用 id
  • nameddevelopment下 的默认值,一个可读的(你能看的懂得)名称的 id
  • deterministic:确定性的,在不同的编译中不变的短数字 id
    • webpack4 中是没有这个值的;
    • 那个时候如果使用 natural,那么在一些编译发生变化时,就需要重新进行打包就会有问题;

最佳实践:

  • 开发过程中,我们推荐使用 named
  • 打包过程中,我们推荐使用 deterministic

1.5.3. runtimeChunk 的配置

配置 runtime 相关的代码是否抽取到一个单独的 chunk 中:

  • runtime 相关的代码指的是在运行环境中,对模块进行解析、加载、模块信息相关的代码
  • 比如我们的 index 中通过 import 函数相关的代码加载,就是通过 runtime 代码完成的; 抽离出来后,有利于浏览器缓存的策略:
  • 比如我们修改了业务代码(main),那么 runtimecomponentbarchunk 是不需要重新加载的;
  • 比如我们修改了 componentbar 的代码,那么 main 中的代码是不需要重新加载的; 设置的值:
  • true/multiple:针对每个入口打包一个 runtime 文件;
  • single:打包一个 runtime 文件;
  • 对象:name 属性决定 runtimeChunk 的名称;

对于每个 runtime chunk,导入的模块会被分别初始化,因此如果你在同一个页面中引用多个入口起点,请注意此行为。你或许应该将其设置为 single,或者使用其他只有一个 runtime 实例的配置。

1.5.4. Prefetch 和 Preload

webpack v4.6.0+ 增加了对预获取和预加载的支持。

在声明 import 时,使用下面这些内置指令,来告知浏览器:

  • prefetch(预获取):将来某些导航下可能需要的资源
  • preload(预加载):当前导航下可能需要资源

image.png prefetch 指令相比,preload 指令有许多不同之处:

  • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
  • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
  • preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。

推荐使用 prefetch ,因为它是在未来闲置的时候下载,有些东西是不需要立即下载的,这样做不会因为请求不重要的资源而占用网络带宽。

2. Shimming 预制依赖

shimming 是一个概念,是某一类功能的统称:

  • 翻译过来我们称之为 垫片,相当于给我们的代码填充一些垫片来处理一些问题;
  • 比如我们现在依赖一个第三方的库,这个第三方的库本身依赖 lodash ,但是默认没有对 lodash 进行导入(认为全局存在 lodash),那么我们就可以通过 ProvidePlugin 来实现 shimming 的效果;

注意webpack 并不推荐随意的使用 shimmingWebpack 背后的整个理念是使前端开发更加模块化;也就是说,需要编写具有封闭性的、不存在隐含依赖(比如全局变量)的彼此隔离的模块;

2.1 Shimming 预支全局变量

假如一个文件中我们使用了 axios,但是没有对它进行引入,那么下面的代码是会报错的;

js
axios.get('XXXXX').then(res => { console.log(res) }) get('XXXXX').then(res => { console.log(res) })

我们可以通过使用 ProvidePlugin 来实现 shimming 的效果:

  1. 修改 webpack.config.js
js
new ProvidePlugin({ axios: 'axios', get: ['axios','get'] })
  • ProvidePlugin 能够帮助我们在每个模块中,通过一个变量来获取一个 package
  • 如果 webpack 看到这个模块,它将在最终的 bundle 中引入这个模块;
  • 另外 ProvidePluginwebpack默认的一个插件,所以不需要专门导入;

这段代码的本质是告诉webpack: 如果你遇到了至少一处用到 axios 变量的模块实例,那请你将 axios package 引入进来,并将其提供给需要用到它的模块。

3. TerserPlugin 代码压缩

在了解 TerserPlugin 插件前,我们先来认识一下什么是 Terser

3.1 Terser 介绍

什么是 Terser 呢?

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

也就是说,Terser 可以帮助我们压缩、丑化我们的代码,让我们的 bundle 变得更小。

我们现在就来用一下 Terser,因为 Terser 是一个独立的工具,所以它可以单独安装:

# 全局安装 npm install terser -g # 局部安装 npm install terser -D

可以在命令行中使用 Terser:

terser [input files] [options] # 举例说明 terser js/file1.js -o foo.min.js -c -m

image.png

我们这里来讲解几个 Compress optionMangle(乱砍) option

Compress option

  • arrows:class或者object中的函数,转换成箭头函数;
  • arguments:将函数中使用 arguments[index]转成对应的形参名称;
  • dead_code:移除不可达的代码(tree shaking);

Mangle option

  • toplevel:默认值是false,顶层作用域中的变量名称,进行丑化(转换);
  • keep_classnames:默认值是false,是否保持依赖的类名称;
  • keep_fnames:默认值是false,是否保持原来的函数名称;

3.2 Terser 在 webpack 中配置(JS 的压缩)

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

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

修改 webpack.config.js 配置:

js
const TerserPlugin = require("terser-webpack-plugin"); ... optimization: { // 打开minimize,让其对我们的代码进行压缩(默认production模式下已经打 minimize: true, minimizer: [ new TerserPlugin({ // extractComments:默认值为true,表示会将注释抽取到一个单独的文件中; // 在开发中,我们不希望保留这个注释时,可以设置为false; extractComments: false, // parallel:使用多进程并发运行提高构建的速度,默认值是true // 并发运行的默认数量: os.cpus().length - 1; // 我们也可以设置自己的个数,但是使用默认值即可; // parallel: true, // terserOptions:设置我们的terser相关的配置 terserOptions: { // 设置压缩相关的选项; compress: { unused: false, }, // 设置丑化相关的选项,可以直接设置为true; mangle: true, // 顶层变量是否进行转换; toplevel: true, // 保留类的名称; keep_classnames: true, // 保留函数的名称; keep_fnames: true, }, }), ], },

3.3 CSS 的压缩

上面我们讲了 JS 的代码压缩,而在我们的前端项目中另一类占大头的代码就是 CSS

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

image.png

4. Tree Shaking

什么是 Tree Shaking

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

JavaScriptTree Shaking

  • JavaScript 进行 Tree Shaking 是源自打包工具 rollup
  • 这是因为 Tree Shaking 依赖于 ES Module 的静态语法分析(不执行任何的代码,可以明确知道模块的依赖关系);
  • webpack2 正式内置支持了 ES2015 模块,和检测未使用模块的能力;
  • webpack4 正式扩展了这个能力,并且通过 package.jsonsideEffects 属性作为标记,告知 webpack 在编译时,哪里文件可以安全的删除掉;
  • webpack5 中,也提供了对部分 CommonJStree shaking 的支持; ✓ https://github.com/webpack/changelog-v5#commonjs-tree-shaking

4.1 webpack 实现 Tree Shaking

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

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

usedExportssideEffects 这两个东西的优化是不同的事情。

引用官方文档的话: The sideEffects and usedExports(more konwn as tree shaking)optimizations are two different things

下面我们分别来演示一下这两个属性的使用

4.1.1 usedExports

  1. 新建一个 webpack-demo
shell
mkdir webpack-demo cd webpack-demo npm init -y npm install webpack webpack-cli lodash --save-dev
  1. 创建 src/math.js 文件:
js
export const add = (num1, num2) => num1 + num2; export const sub = (num1, num2) => num1 - num2;

在这个问价中仅是导出了两个函数方法

  1. 创建 src/index.js 文件:、
js
import { add, sub } from "./math"; console.log(add(1, 2));

index.js 中 导入了刚刚创建的两个函数,但是只使用了 add

  1. 配置 webpack.config.js
js
const path = require("path"); module.exports = { mode: "development", devtool: false, entry: "./src/index.js", output: { path: path.resolve(__dirname, "dist"), filename: "main.js", }, optimization: { usedExports: true, }, };

为了可以看到 usedExports 带来的效果,我们需要设置为 development 模式。因为在 production 模式下,webpack 默认的一些优化会带来很大的影响。

  1. 设置 usedExportstruefalse 对比打包后的代码:

image.png

image.png

仔细观察上面两张图可以发现当设置 usedExports: true 时,sub 函数没有导出了,另外会多出一段注释:unused harmony export mul;这段注释的意义是会告知 Terser 在优化时,可以删除掉这段代码。

这个时候,我们将 minimize 设置 true

image.png

image.png

  • usedExports 设置为 false 时,sub 函数没有被移除掉;
  • usedExports 设置为 true 时,sub 函数有被移除掉;

所以,usedExports 实现 Tree Shaking 是结合 Terser 来完成的。

4.1.2 sideEffects

在一个纯粹的 ESM 模块世界中,很容易识别出哪些文件有副作用。然而,我们的项目无法达到这种纯度,所以,此时有必要提示 webpack compiler 哪些代码是“纯粹部分”。

通过 package.json"sideEffects" 属性,来实现这种方式。

json
{ "name": "your-project", "sideEffects": false }

如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false,来告知 webpack 它可以安全地删除未用到的 export

"side effect(副作用)" 的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export

如果你的代码确实有一些副作用,可以改为提供一个数组:

json
{ "name": "your-project", "sideEffects": ["./src/some-side-effectful-file.js"] }

注意,所有导入文件都会受到 tree shaking 的影响。这意味着,如果在项目中使用类似 css-loaderimport 一个 CSS 文件,则需要将其添加到 side effect 列表中,以免在生产模式中无意中将它删除:

json
{ "name": "your-project", "sideEffects": ["./src/some-side-effectful-file.js", "*.css"] }

4.2 CSS 实现 Tree Shaking

上面将的都是关于 JavaScriptTree Shaking ,对于 CSS 同样有对应的 Tree Shaking 操作。

  • 在早期的时候,我们会使用 PurifyCss 插件来完成 CSStree shaking,但是目前该库已经不再维护了(最新更新也是在 4 年前了);

  • 目前我们可以使用另外一个库来完成 CSSTree ShakingPurgeCSS,也是一个帮助我们删除未使用的 CSS 的工具;

  1. 安装 PurgeCsswebpack 插件:
shell
npm install purgecss-webpack-plugin -D
  1. webpack.config.js 中配置 PurgeCss
new PurgeCSSPlugin({ paths: glob.sync(`${path.resolve(__dirname, '../src')}/**/*`, { nodir: true }), only: ['bundle', 'vendor'] })
  • paths:表示要检测哪些目录下的内容需要被分析,这里我们可以使用 glob
  • 默认情况下,Purgecss 会将我们的 html 标签的样式移除掉,如果我们希望保留,可以添加一个 safelist 的属性;

purgecss 也可以对 lesssass文件进行处理(它是对打包后的 css 进行 tree shaking 操作);

4.3 Scope Hoisting

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

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

  • 无论是从最开始的代码运行,还是加载一个模块,都需要执行一系列的函数
  • Scope Hoisting 可以将函数合并到一个模块中来运行;(作用域提升,在主模块里直接运行它,而不是去加载一些单独的模块)

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

  • production 模式下,默认这个模块就会启用;
  • development 模式下,我们需要自己来打开该模块;
js
new webpack.optimize.ModuleConcatenationPlugin()

5. webpack 对文件压缩

经过前几小节的代码压缩优化(Tree Shaking 的优化、Terser 的优化、CSS 压缩的优化),基本上已经没有什么可以通过删除一些代码再压缩文件的方法了(变量、空格、换行符、注释、没用的代码都已经处理了)

但是我们还有一种通过压缩算法从对文件压缩的方式来继续减小包的体积(就像在 winodows 将文件夹压缩成 zip 一样,只不过我们这里是对单个js文件进行压缩)

目前的压缩格式非常的多:

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

webpack 中的配置:

  1. 安装 CompressionPlugin
shell
npm install compression-webpack-plugin -D
  1. 配置 webpack.config.js
js
new CompressionPlugin({ test: /].(css|js)$/, // 匹配哪些文件需要压缩 // threshold: 500, // 设置文件从多大开始压缩 minRatio: 0.7, // 至少的压缩比例 algorithm: "gzip, // 才用的压缩算法 // include // exclude })

6. HTML 文件中代码的压缩

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

  • inject:设置打包的资源插入的位置
    • true、 false 、body、head
  • cache:设置为true,只有当文件改变时,才会生成新的文件(默认值也是true)
  • minify:默认会使用一个插件html-minifier-terser

image.png

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:叶继伟

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!