❝打不垮我的,将使我更加坚强 --尼采
❞
大家好,我是「柒八九」。
好久没更文了,其实这段时间,一直没闲着。在准备一些比较重要的东西。忙着跑步,忙着学习,忙着xx。 总之就是,一直在忙着,从未停歇。
虽然,这段时间,没有文章的发布,其实,在私底下,已经有不下10篇的文章已经起手了。等再润色一下,就会和大家见面。

这是,我之前学习总结,后期会逐步给大家免费分享。敬请期待。
好了,闲话少叙。
今天,我们来谈谈关于-- Webpack的打包优化。

你能所学到的知识点
❝❞
Webpack Loader
和Plugin
的区别Webpack 生命周期 Webpack 「编译阶段」提效
「减少」执行编译的模块 提升 「单个模块」构建的速度 「并行构建」以提升总体效率 Webpack 「打包阶段」提效
以提升 「当前任务」工作效率为目标的方案
「压缩」 Chunk 产物代码 以提升 「后续环节」工作效率为目标的方案
Code Splitting
Tree Shaking
Scope Hoisting
(作用域提升)sideEffects
缓存优化
Webpack Loader vs Plugin
-
loader
是 「文件加载器」,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中 -
plugin
赋予了webpack
各种灵活的功能,例如打包优化、资源管理、环境变量注入等,目的是 「解决 loader 无法实现的其他事」

两者在运行时机上的区别
-
loader
运行在 「打包文件之前」 -
plugins
在整个编译周期都起作用
对于 loader
,实质是一个「转换器」,将A文件进行编译形成B文件,操作的是文件,比如将A.scss
或A.less
转变为B.css
,「单纯的文件转换」过程。
在 Webpack
运行的生命周期中会广播出许多事件,Plugin
可以监听这些事件,在合适的时机通过Webpack提供的 API改变输出结果。
Webpack 生命周期
Webpack
工作流程中最核心的两个模块
-
Compiler
-
Compilation
它们都扩展自 Tapable
类,用于实现工作流程中的生命周期划分,以便在不同的生命周期节点上注册和调用插件,其中所暴露出来的生命周期节点称为Hook
(俗称钩子)。
Compiler Hooks
「构建器实例」的生命周期可以分为 3 个阶段
-
初始化阶段 -
构建过程阶段 -
产物生成阶段
初始化阶段
-
environment
-
在创建完 compiler
实例且执行了配置内定义的插件的apply
方法后触发
-
-
afterEnvironment
-
在创建完 compiler
实例且执行了配置内定义的插件的apply
方法后触发
-
-
entryOption
-
执行 EntryOptions
插件
-
-
afterPlugins
-
afterResolvers
-
解析了 resolver
配置后触发
-
构建过程阶段
-
normalModuleFactory
-
在两类 模块工厂创建后触发
-
-
contextModuleFactory
-
在两类 模块工厂创建后触发
-
-
beforeRun
-
run
-
beforeCompile
-
compile
-
thisCompilation
-
「 make
」-
「最耗时」 -
会执行模块编译到优化的完整过程
-
产物生成阶段
-
shouldEmit、emit、assetEmitted、afterEmit
-
在构建完成后,处理产物的过程中触发
-
-
failed、done
-
在达到最终结果状态时触发
-
Compilation Hooks
「构建过程实例」的生命周期分为两个阶段:
-
构建阶段 -
优化阶段
Webpack编译阶段提效
❝真正影响整个构建效率的是
Compilation
实例的处理过程❞
「编译模块」 「优化处理」
要「提升编译阶段的构建效率」,大致可以分为三个方向
-
「减少」执行编译的模块 -
提升 「单个模块」构建的速度 -
「并行构建」以提升总体效率
优化前的准备工作
准备基于时间的分析工具 - SMP
需要一类插件,来帮助我们统计项目构建过程中在编译阶段的耗时情况
speed-measure-webpack-plugin
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const webpackConfig = smp.wrap({
plugins: [new MyPlugin(), new MyOtherPlugin()],
});
准备基于产物内容的分析工具 - WBA
找出对产物包体积影响最大的包的构成,从而找到那些冗余的、可以被优化的依赖项。不仅能减小最后的包体积大小,也能提升构建模块时的效率 webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
❝编译模块阶段所耗的时间是从单个入口点开始,编译每个模块的时间的总和
❞
减少执行编译的模块(4个)
-
IgnorePlugin
(国际化包) -
按需引入类库模块 (工具类库) -
DllPlugin
-
Externals
IgnorePlugin
有的依赖包,除了项目所需的模块内容外,还会附带一些多余的模块

Webpack 提供的 IgnorePlugin
,即可在「构建模块时」直接剔除那些需要被排除的模块,从而提升构建模块的速度,并减少产物体积。
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
});
-
resourceRegExp -
指定需要剔除的文件(夹)
-
-
contextRegExp (可选) -
特定目录
-
-
任何以 'moment' 结尾的目录中匹配 './locale' 的任何 require
语句都将被忽略
除了 moment
包以外,其他一些带有「国际化模块」的依赖包,都可以应用这一优化方式。
按需引入类库模块
「减少执行模块的方式是按需引入」,一般适用于「工具类库」性质的依赖包的优化

lodash
依赖包
优化处理
-
定向引入 -
效果最佳的方式是在 「导入声明时只导入依赖包内的特定模块」
-
-
使用插件 -
babel-plugin-lodash
-
babel-plugin-import
-
适用于 antd
,antd-mobil
,lodash
-
-
{
"plugins": [["import",{
"libraryName": "lodash",
"libraryDirectory": "",
"camel2DashComponentName": false, // default: true
}]]
}
注意点
Tree Shaking
,这一特性也能减少产物包的体积,但是 Tree Shaking
需要相应导入的依赖包使用 ES6
模块化,而 lodash
还是基于 CommonJS
,需要替换为 lodash-es
才能生效
Tree Shaking
是在优化阶段生效,Tree Shaking
并不能减少模块编译阶段的构建时间。
DllPlugin
它的核心思想是将项目依赖的框架等模块「单独构建打包」,与普通构建流程区分开。
事先把常用但又构建时间长的代码提前打包好(例如 react
、react-dom
),取个名字叫 dll
。后面再打包的时候就跳过原来的未打包代码,直接用 dll
。这样一来,构建时间就会缩短,提高 webpack
打包速度。
两个配置文件
webpack.dll.config.js
module.exports = {
entry: {
vendor: ['react', 'react-dom'],
},
output: {
filename: '[name].dll.js',
path: path.join(__dirname, 'dll'),
publicPath: '/dll',
library: '[name]_dll',
},
plugins: [
new webpack.DllPlugin({
context: __dirname,
name: '[name]_dll',
path: path.join(__dirname, 'dll' + '/[name]_manifest.json'),
}),
],
}
new webpack.DllPlugin
- 生成manifest.json
文件,供DllReferencePlugin
指向依赖模块位置 - 将公共模块 react/react-dom
抽离到项目中dll文件下
webpack.app.config.js
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./dll/vendor_manifest.json'),
}),
],
new webpack.DllReferencePlugin
-
引用 manifest.json
文件,寻找依赖模块
❝webpack 4 有着比 dll 更好的打包性能,所以在最新版的cra中已经将dll剔除。
❞
Externals
Webpack 配置中的 externals
和 DllPlugin
解决的是同一类问题。「将依赖的框架等模块从构建过程中移除」。
Externals
和 DllPlugin
区别
-
配置方面 -
externals
更简单 -
DllPlugin
需要独立的配置文件
-
-
DllPlugin
包含了依赖包的独立构建流程,而externals
配置中不包含依赖框架的生成方式,通常使用已传入 CDN 的依赖包 -
externals
配置的依赖包需要单独指定依赖模块的加载方式:全局对象、CommonJS
、AMD
等 -
在引用依赖包的子模块时, DllPlugin
无须更改,而externals
则会将子模块打入项目包中
使用范例
module.exports = {
//...
externals: [
{
// String
react: 'react',
// Object
lodash: {
commonjs: 'lodash',
amd: 'lodash',
root: '_', // indicates global variable
},
// [string]
subtract: ['./math', 'subtract'],
},
// Function
function ({ context, request }, callback) {
if (/^yourregex$/.test(request)) {
return callback(null, 'commonjs ' + request);
}
callback();
},
// Regex
/^(jquery|\$)$/i,
],
};
提升单个模块构建的速度
在保持构建模块数量不变的情况下,提升单个模块构建的速度。
常用的方式有
-
include/exclude
-
noParse
-
Source Map
-
TypeScript 编译优化 -
Resolve
通过减少构建「单个模块」时的一些处理逻辑来提升速度
include/exclude
Webpack -loader
配置中的 include/exclude
,是常用的优化特定模块构建速度的方式之一
-
include
的用途是只对符合条件的模块使用指定Loader
进行转换处理 -
exclude
则相反,不对特定条件的模块使用该Loader
例如不使用 babel-loader 处理 node_modules 中的模块 使用范例
module.exports = {
......
module: {
rules: [
{
test: /\.js$/,
include: /src/
exclude: /node_modules/,
use: ['babel-loader'],
},
],
},
}
「注意点」
-
通过 include/exclude
排除的模块,并非不进行编译,而是使用Webpack
「默认的 js 模块编译器进行编译」 -
在一个 loader
中的include
与exclude
配置存在冲突的情况下,优先使用exclude
的配置,而忽略冲突的include
部分的配置
noParse
Webpack 配置中的 module.noParse
则是在 include/exclude
的基础上,进一步省略了使用默认 js 模块编译器进行编译的时间
使用范例
module.exports = {
......
module: {
noParse: /jquery|lodash/,
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
},
],
},
}
Source Map
对于生产环境的代码构建而言,会根据项目实际情况判断是否开启 Source Map
在开启 Source Map
的情况下,「优先选择与源文件分离的类型」 --例如 "source-map"
TypeScript 编译优化
Webpack 中编译 TS 有两种方式
-
使用 ts-loader
-
使用 babel-loader
在使用 ts-loader
时,由于 ts-loader
默认在「编译前进行类型检查,因此编译时间往往比较慢」
通过加上配置项 transpileOnly: true
,可以在编译时忽略类型检查
module.exports = {
......
module: {
rules: [
{
test: /\.ts$/,
use: {
loader: 'ts-loader',
options: {
transpileOnly: true,
},
},
},
],
},
}
babel-loader
则需要单独安装 @babel/preset-typescript
来支持编译 TS,配合 ForkTsCheckerWebpackPlugin
使用类型检查功能
module.exports = {
......
module: {
rules: [
{
test: /\.ts$/,
use: ['babel-loader'],
},
],
},
plugins: [
new TSCheckerPlugin({
typescript: {
diagnosticOptions: {
semantic: true,
syntactic: true,
},
},
}),
],
}
Resolve
Webpack
中的 resolve
配置制定的是在「构建时指定查找模块文件的规则」
-
resolve.modules
-
指定查找模块的目录范围
-
-
resolve.extensions
-
指定查找模块的文件类型范围
-
-
resolve.mainFields
-
指定查找模块的 package.json
中主文件的属性名
-
-
resolve.symlinks
-
指定在查找模块时是否处理软连接
-
这些规则「在处理每个模块时都会有所应用」,因此尽管对小型项目的构建速度来说影响不大,对于大型的模块众多的项目而言,「使用默认配置和增加了大量无效范围后,构建时长的变化」。
并行构建以提升总体效率
「并行构建」的方案早在 Webpack 2
时代已经出现,适用于大项目。 使用方式
-
HappyPack -
thread-loader -
parallel-webpack
HappyPack 与 thread-loader
两种工具的本质作用相同,都作用于模块编译的 Loader
上,「用于在特定 Loader 的编译过程中」。
开启多进程的方式加速编译
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve('src'),
use: [
'thread-loader',
’babel-loader‘
],
},
],
},
};
parallel-webpack
并发构建的第二种场景是「针对与多配置构建」。
Webpack
的配置文件可以是「一个包含多个子配置对象的数组」,在执行这类多配置构建时,默认「串行执行」
var path = require('path');
module.exports = [
{
entry: './pageA.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'pageA.bundle.js'
}
},
{
entry: './pageB.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'pageB.bundle.js'
}
}];
通过 parallel-webpack
,就能「实现相关配置的并行处理」
"build:parallel": "parallel-webpack --config webpack.parallel.config.js"
Webpack打包阶段提效
Webpack
构建流程中的第二个阶段,也就是从代码优化到生成产物阶段的效率提升问题
优化阶段可以分为两个不同的方向
-
针对 「某些任务」 -
使用效率更高的工具或配置项 -
从而 「提升当前任务的工作效率」
-
-
提升 「特定任务」的优化效果 -
以 「减少传递给下一任务的数据量」 -
从而提升后续环节的工作效率
-
以提升当前任务工作效率为目标的方案
一般在项目的优化阶段,主要耗时的任务有两个
-
生成 ChunkAssets
-
即根据 Chunk
信息生成 Chunk 的产物代码 -
主要在 Webpack
引擎内部的模块中处理-
优化手段较少 -
主要集中在 「利用缓存」方面
-
-
-
优化 Assets -
即 「压缩」 Chunk 产物代码
-
面向 JS 的压缩工具
Webpack 4 中内置了 TerserWebpackPlugin
作为默认的 JS 压缩工具--基于 Terser
。之前的版本,需要单独引入,早期主要使用的是 UglifyJSWebpackPlugin
-- 基于 UglifyJS
。两者在压缩效率与质量方面差别不大,但 Terser
整体上略胜一筹
Terser 和 UglifyJS 插件中的效率优化
Terser
原本是 Fork
自 uglify-es
的项目,其绝大部分的 API 和参数都与 uglify-es 和 uglify-js@3 兼容。
以 Terser 为例来分析其中的优化方向
npm install terser-webpack-plugin --save-dev
TerserWebpackPlugin
中,对执行效率产生影响的配置主要分为 3 个方面
-
Cache
选项-
默认开启 -
使用缓存能够极大程度上提升再次构建时的工作效率
-
-
Parallel
选项-
默认开启 -
并发选项在大多数情况下能够提升该插件的工作效率 -
适用大项目
-
-
terserOptions
选项-
即 Terser
工具中的minify
选项集合 -
主要看其中的 compress
和mangle
选项 -
compress
参数的作用-
执行特定的压缩策略 -
例如省略变量赋值的语句,从而将变量的值直接替换到引入变量的位置上,减小代码体积 -
在需要对压缩阶段的效率进行优化的情况下,可以优先选择设置该参数
-
-
mangle
参数的作用-
对源代码中的变量与函数名称进行压缩
-
-
当compress参数为 false 时,压缩阶段的效率有明显提升,同时对压缩的质量影响较小
-
案例使用
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
cache: false,
terserOptions: {
compress: false,
mangle: false,
},
}),
],
},
};
压缩代码是在 optimizeChunkAssets 阶段
面向 CSS 的压缩工具
CSS 同样有3种压缩工具可供选择
-
OptimizeCSSAssetsPlugin
-
CRA中使用
-
-
OptimizeCSSNanoPlugin
-
vue-cli
-
-
CssMinimizerWebpackPlugin
-
2020 年 Webpack
社区新发布的 CSS 压缩插件
-
它们都基于 cssnano
实现,「压缩质量方面」没有什么差别。
在压缩效率方面,最新发布的 MiniCssExtractPlugin
,它支持缓存和多进程,「默认开启多进程」。这是另外两个工具不具备的。
MiniCssExtractPlugin
对于 CSS
文件的打包,一般我们会使用 style-loader
进行处理,这种处理方式最终的打包结果就是 CSS
代码会内嵌到 JS 代码中
MiniCssExtractPlugin
是一个可以将 CSS
代码从打包结果中提取出来的插件。
// ./webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader', // 将样式通过 style 标签注入
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin()
]
}
将这个插件添加到配置对象的 plugins
数组中,使用 MiniCssExtractPlugin
中提供的 loader
去替换掉 style-loader
,以此来「捕获到所有的样式」。打包过后,样式就会存放在独立的文件中,直接通过 link
标签引入页面
CssMinimizerWebpackPlugin (webpack 5)
使用了 MiniCssExtractPlugin
过后,样式就被提取到单独的 CSS 文件中了,「样式文件并没有被压缩」。Webpack
「内置的压缩插件仅仅是针对 JS 文件的压缩,其他资源文件的压缩都需要额外的插件」。
// ./webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
optimization: {
minimizer: [
new CssMinimizerPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin()
]
}
文档中的这个插件并不是配置在 plugins
数组中的,而是添加到了 optimization
对象中的 minimizer
属性中。
❝「如果我们配置到
❞plugins
属性中,那么这个插件在任何情况下都会工作,而配置到 minimizer 中,就只会在 minimize 特性开启时才工作」 ---optimization.minimize: true
原本可以自动压缩的 JS,现在却不能压缩了,因为设置了 minimizer
。Webpack
认为我们需要使用自定义压缩器插件,那内部的 JS 压缩器就会被覆盖掉。「必须手动再添加回来」
内置的 JS 压缩插件叫作 terser-webpack-plugin
,手动添加这个模块到 minimizer
配置当中。
// ./webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserWebpackPlugin(),
new CssMinimizerPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin()
]
}
以提升后续环节工作效率为目标的方案
❝通过对本环节的处理减少后续环节处理内容,以便提升后续环节的工作效率
❞
-
Code Splitting
-
Tree Shaking
-
Scope Hoisting
(作用域提升) -
sideEffects
Code Splitting(分块打包)
Code Splitting--通过把项目中的资源模块按照我们设计的规则打包到不同的 bundle
中
-
降低应用的启动成本 -
提高响应速度
Webpack 实现分包的方式主要有两种
-
根据 「业务不同配置多个打包入口」,输出多个打包结果 -
结合 ES Modules
的动态导入(Dynamic Imports
)特性, 「按需加载模块」
多入口打包
多入口打包一般适用于传统的多页应用程序,最常见的划分规则就是:一个页面对应一个打包入口,对于不同页面间公用的部分,再提取到公共的结果中
├── dist
├── src
│ ├── common
│ │ ├── fetch.js
│ │ └── global.css
│ ├── album.css
│ ├── album.html
│ ├── album.js
│ ├── index.css
│ ├── index.html
│ └── index.js
├── package.json
└── webpack.config.js
有两个页面,分别是 index
和 album
-
index.js 负责实现 index 页面功能逻辑 -
album.js 负责实现 album 页面功能逻辑 -
global.css 是公用的样式文件 -
fetch.js 是一个公用的模块,负责请求 API
配置文件
// ./webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
filename: '[name].bundle.js' // [name] 是入口名称
},
// ... 其他配置
plugins: [
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html'
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html'
})
]
}
-
一般
entry
属性中只会配置一个打包入口。-
如果需要配置多个入口,可以把 entry
「定义成一个对象」。 -
entry
是定义为对象而不是数组,如果是数组的话就是把多个文件打包到一起,还是一个入口。 -
这个对象中一个属性就是一个入口, 「属性名称就是这个入口的名称,值就是这个入口对应的文件路径」。
-
-
输出文件名 - 使用
[name]
这种占位符来输出动态的文件名 -[name]
最终会被替换为入口的名称 -
通过
html-webpack-plugin
- 分别为index
和album
页面生成了对应的 HTML 文件
分包加载
输出 HTML 的插件,默认这个插件会自动注入所有的打包结果。如果需要指定所使用的 bundle
,通过 HtmlWebpackPlugin
的 chunks
属性来设置
「每个打包入口都会形成一个独立的 chunk(块)」
// ./webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
filename: '[name].bundle.js' // [name] 是入口名称
},
// ... 其他配置
plugins: [
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
chunks: ['index'] // 指定使用 index.bundle.js
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html',
chunks: ['album'] // 指定使用 album.bundle.js
})
]
}
提取公共模块
需要把这些公共的模块提取到一个单独的 bundle 中
优化配置中开启 splitChunks
功能
// ./webpack.config.js
module.exports = {
entry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
filename: '[name].bundle.js' // [name] 是入口名称
},
optimization: {
splitChunks: {
// 自动提取所有公共模块到单独 bundle
chunks: 'all'
}
}
// ... 其他配置
}
将它设置为 all
,表示所有公共模块都可以被提取
动态导入
Code Splitting
更常见的实现方式还是「结合 ES Modules 的动态导入特性,从而实现按需加载」。
一般我们常说的按需加载指的是加载数据或者加载图片,这里所说的按需加载,指的是「在应用运行过程中,需要某个资源模块时,才去加载这个模块」。
-
极大地 「降低了应用启动时需要加载的资源体积」 -
提高了应用的 「响应速度」 -
节省了 「带宽和流量」
Webpack
中支持使用动态导入的方式实现模块的按需加载,而且「所有动态导入的模块都会被自动提取到单独的 bundle 中,从而实现分包」
├── src
│ ├── album
│ │ ├── album.css
│ │ └── album.js
│ ├── common
│ │ ├── fetch.js
│ │ └── global.css
│ ├── posts
│ │ ├── posts.css
│ │ └── posts.js
│ ├── index.html
│ └── index.js
├── package.json
└── webpack.config.js
文章列表对应的是这里的 posts
组件,而相册列表对应的是 album
组件
在打包入口(index.js)中同时导入了这两个模块,然后根据页面锚点的变化决定显示哪个组件
// ./src/index.js
// import posts from './posts/posts'
// import album from './album/album'
const update = () => {
const hash = window.location.hash || '#posts'
const mainElement = document.querySelector('.main')
mainElement.innerHTML = ''
if (hash === '#posts') {
// mainElement.appendChild(posts())
import('./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
})
} else if (hash === '#album') {
// mainElement.appendChild(album())
import('./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
})
}
}
window.addEventListener('hashchange', update)
update()
为了动态导入模块,可以将 import
关键字作为函数调用。当以这种方式使用时,import
函数返回一个 Promise
对象.
-
在需要使用组件的地方通过 import
函数导入指定路径 -
方法返回的是一个 Promise
-
Promise
的then
方法中能够拿到模块对象
由于这里的 posts 和 album 模块是「以默认成员导出,需要解构模块对象中的 default」,先拿到导出成员,然后再正常使用这个导出成员。
import('./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
})
魔法注释
默认通过动态导入产生的 bundle
文件,它的 name
就是一个序号。如果需要给这些 bundle
命名的话,就可以使用 Webpack 所特有的魔法注释去实现
import(/* webpackChunkName: 'posts' */'./posts/posts')
.then(({ default: posts }) => {
mainElement.appendChild(posts())
})
所谓魔法注释,就是在 import
函数的「形式参数位置,添加一个行内注释」,注释有一个特定的格式---webpackChunkName:’xxx‘
,就可以给分包的 chunk
起名字
如果 chunkName
相同的话,那相同的 chunkName
最终就会被打包到一起,借助这个特点,就可以根据自己的实际情况,灵活组织动态加载的模块了。
Tree Shaking
Tree-shaking
最早是 Rollup
中推出的一个特性,Webpack 从 2.0 过后开始支持这个特性。使用 Webpack
生产模式打包的优化过程中,自动开启这个功能 --- npx webpack --mode=production
其他模式开启 Tree Shaking
配置对象中添加一个 optimization
属性,该属性用来「集中配置 Webpack 内置优化功能」,它的值也是一个对象,在 optimization
对象中先开启一个 usedExports
选项,表示在输出结果中只导出外部使用了的成员
module.exports = {
// ... 其他配置项
optimization: {
// 模块只导出被使用的成员
usedExports: true
}
}
对于未引用代码,如果我们开启压缩代码功能,就可以自动压缩掉这些没有用到的代码.
module.exports = {
// ... 其他配置项
optimization: {
// 模块只导出被使用的成员
usedExports: true,
// 压缩输出结果
minimize: true
}
}
Tree-shaking 的实现,整个过程用到了 Webpack 的两个优化功能
-
usedExports
-
打包结果中只导出外部用到的成员
-
-
minimize
-
压缩打包结果
-
把代码看成一棵大树
-
usedExports
的作用就是标记树上哪些是枯树枝、枯树叶 -
minimize
的作用就是负责把枯树枝、枯树叶摇下来
结合 babel-loader 的问题
Tree-shaking
实现的前提是 ES Modules
,最终交给 Webpack
打包的代码,「必须是使用 ES Modules
的方式来组织的模块化」
Webpack 在打包所有的模块代码之前
-
先是将模块根据配置交给不同的 Loader
处理 -
最后再将 Loader
处理的结果打包到一起
为了更好的兼容性,会选择使用 babel-loader
去转换我们源代码中的一些 ECMAScript
的新特性,Babel
在转换 JS 代码时,很有可能处理掉代码中的 ES Modules
部分,把它们转换成 CommonJS
的方式。
babel-loader
(低版本)
我们为 Babel
配置的都是一个 preset
(预设插件集合),而不是某些具体的插件。
目前市面上使用最多的 @babel/preset-env
,这个预设里面就有转换 ES Modules
的插件。使用这个预设时,代码中的 ES Modules
部分就会被转换成 CommonJS
方式。Webpack 再去打包时,拿到的就是以 CommonJS
方式组织的代码了,所以 Tree-shaking
不能生效
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env']
]
}
}
}
]
},
optimization: {
usedExports: true
}
}
最新版本(8.x)的 babel-loader
「自动帮我们关闭了对 ES Modules 转换的插件」,经过 babel-loader
处理后的代码默认仍然是 ES Modules
。那 Webpack
最终打包得到的还是 ES Modules
代码。Tree-shaking
自然也就可以正常工作了
最新版本的 babel-loader
并不会导致 Tree-shaking
失效,确保babel-loader
能使用Tree-shaking
。最简单的办法就是在配置中将 @babel/preset-env
的 modules
属性设置为 false
。确保不会转换 ES Modules
,也就确保了 Tree-shaking
的前提
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { modules: 'false' }]
]
}
}
}
]
},
optimization: {
usedExports: true
}
}
Scope Hoisting (作用域提升)
Webpack 3.0 中添加的一个特性,使用 concatenateModules
选项继续优化输出
普通打包只是将一个模块最终放入一个单独的函数中,如果模块很多,就意味着在输出结果中会有很多的模块函数。concatenateModules
配置的作用,尽可能将所有模块合并到一起输出到一个函数中,既提升了运行效率,又减少了代码的体积。
module.exports = {
// ... 其他配置项
optimization: {
// 模块只导出被使用的成员
usedExports: true,
// 尽可能合并每一个模块到一个函数中
concatenateModules: true,
}
}
bundle.js
中就不再是一个模块对应一个函数了,而是把所有的模块都放到了一个函数中
sideEffects
Webpack 4 中新增了一个 sideEffects
特性,允许「通过配置标识我们的代码是否有副作用,从而提供更大的压缩空间」。
模块的副作用指的就是模块执行的时候除了导出成员,是否还做了其他的事情,特性一般只有去开发一个 npm 模块时才会用到。
Tree-shaking
只能移除没有用到的代码成员,而想要「完整移除没有用到的模块」,那就需要开启 sideEffects
特性了,在 optimization
中开启 sideEffects
特性
// ./webpack.config.js
module.exports = {
mode: 'none',
optimization: {
sideEffects: true
}
}
这个特性在 production
模式下同样会自动开启
Webpack 缓存优化
利用缓存数据来「加速构建过程」的处理。
在「初次构建」的压缩代码过程中,就将这一阶段的结果写入了缓存目录(node_modules/.cache/插件名/
)中有缓存。
当「再次构建」进行到压缩代码阶段时,即可对比读取已有缓存。
-
编译阶段的缓存优化 -
优化打包阶段的缓存优化
编译阶段的缓存优化
「编译过程的耗时点主要在使用不同加载器(Loader)来编译模块的过程」
Babel-loader
Babel-loader
是绝大部分项目中会使用到的 JS/JSX/TS
编译器
与缓存相关的设置主要有
-
cacheDirectory
-
「默认为 false
,即不开启缓存」 -
当值为 true
时开启缓存并使用默认缓存目录-
./node_modules/.cache/babel-loader/
-
-
也可以指定其他路径值作为缓存目录
-
-
cacheIdentifier
-
用于计算缓存标识符 -
「默认使用」 -
Babel
相关依赖包的版本 -
babelrc
配置文件的内容 -
环境变量 -
与模块内容
-
-
一起参与计算缓存标识符
-
-
cacheCompression
-
「默认为 true」 -
将缓存内容压缩为 gz 包以减小缓存目录的体积 -
在设为 false
的情况下将跳过压缩和解压的过程,从而提升这一阶段的速度
-
Cache-loader
在编译过程中利用缓存的第二种方式是使用 --- Cache-loader
在使用时,需要「将 cache-loader
添加到对构建效率影响较大的 Loader
(如 babel-loader 等)之前」
module: {
rules: [
{
test: /\.js$/,
use: ['cache-loader', 'babel-loader'],
},
],
}
「使用 cache-loader
后,比使用 babel-loader
的开启缓存选项后的构建时间更短」
主要原因是 babel-loader
中的缓存信息较少,而 cache-loader
中存储的 Buffer 形式的数据处理效率更高。
优化打包阶段的缓存优化
生成 ChunkAsset
时的缓存优化
在 Webpack 4 中,生成 ChunkAsset
过程中的缓存优化是受限制的:
-
只有在 watch
模式下 -
且配置中开启 cache
时(development 模式下自动开启),才能在这一阶段执行缓存的逻辑
在 Webpack 4 中,缓存插件是「基于内存」的,只有在 watch
模式下才能在内存中获取到相应的缓存数据对象
代码压缩时的缓存优化
对于 JS 的压缩TerserWebpackPlugin
/UglifyJSPlugin
都是支持缓存设置的。
对于 CSS 的压缩,目前最新发布的 CSSMinimizerWebpackPlugin
支持且「默认开启缓存」,其他的插件如 OptimizeCSSAssetsPlugin
和 OptimizeCSSNanoPlugin
目前还不支持使用缓存
使用缓存注意点
「如何最大程度地让缓存命中,成为我们选择缓存方案后首先要考虑的」
缓存标识符发生变化导致的缓存失效,支持缓存的 Loader
和插件中,会根据一些「固定字段的值加上所处理的模块或 Chunk 的数据 hash 值来生成对应缓存的标识符」。一旦其中的值发生变化,对应缓存标识符就会发生改变,意味着对应工具中,所有之前的缓存都将失效。需要尽可能少地变更会影响到缓存标识符生成的字段
优化打包阶段的缓存失效,尽管在模块编译阶段每个模块是单独执行编译的。但是当进入到代码压缩环节时,各模块已经被组织到了相关联的 Chunk
中,「N个模块最后只生成了一个 Chunk」。任何一个模块发生变化都会导致整个 Chunk
的内容发生变化,而使之前保存的缓存失效。
优化方案
尽可能地「把那些不变的处理成本高昂的模块打入单独的 Chunk 中」,Webpack
中的分包配置——splitChunks
。使用 splitChunks
「优化缓存利用率」。
好处
-
合并通用依赖 -
提升构建缓存利用率 -
提升资源访问的缓存利用率 -
资源懒加载
CI/CD 中的缓存目录问题
自动化集成的系统中,项目的构建空间会在每次构建执行完毕后,立即回收清理。在这种情况下,「默认的项目构建缓存目录(node_mo dules/.cache)将无法留存」。导致即使项目中开启了缓存设置,也无法享受缓存的便利性,反而因为需要写入缓存文件而浪费额外的时间
如果需要使用缓存,则需要根据对应平台的规范,「将缓存设置到公共缓存目录下」
❝缓存的便利性本质在于用磁盘空间换取构建时间,需要考虑对缓存区域的定期清理
❞
后记
「分享是一种态度」。
参考资料:
-
效率工程化 -
Webpack官网
「全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。」

本文由 mdnice 多平台发布