- 摇树优化
- 作用域提升
- 添加 CSS 前缀
- 压缩 CSS
- 抽取公共模块
- 使用动态导入按需加载
- 忽略对无需解析文件的处理
- 排除无需打包的文件
- 并行构建
- 利用浏览器缓存文件
- 对打包结果进行分析
- 核心内容:
- 几种可视化分析工具
- 具体步骤
- 工具一:webpack-bundle-analyzer 插件的使用
- webpack visualizer 的使用">工具二:webpack visualizer 的使用
- 1、生成统计文件 stats.json
- http://chrisbateman.github.io/webpack-visualizer/,并上传 stats.json">2、 打开工具主页 http://chrisbateman.github.io/webpack-visualizer/,并上传 stats.json
- webpack analyse 的使用">工具三:webpack analyse 的使用
- 预拉取和预加载
摇树优化
定义:
关联性不强的代码会被优化掉,关联性强的代码会留下来到最终打包的文件中
目的:
清理掉没有调用或者可以被忽略的代码,最后可以让打包后的文件体积变得更小
原理:
摇树优化(Tree Shaking),是由 rollup 这个打包工具首先普及的。
它基于 ES6 模块规范中的 import 和 export语法特性,在打包时分析导入/导出内容是否真正的被 JavaScript 上下文所引用,如果没有则从将其从打包结果中移除。
自 webpack 4 开始, production 模式会自动启用各种优化策略(包括摇树优化);但也可手动配置。
步骤大纲
- 创建新目录,并编写入口 js 和两个被入口导入的 js(其中一个含对全局对象window的操作)
- 编写webpack配置文件(devtool:false),打包后观察结果
- 启用
optimization.usedExports: true优化项,打包后观察结果 - 启用
optimization.minimize: true优化项,打包后观察结果 - 通过
package.json中的sideEffects声明代码是否有“副作用”
具体步骤
1、创建新目录,编写以下文件
src/mod1.js
// 两个简单的工具函数export function trim(str) {return str ? str.trim() : ''}export function toUpperCase(str) {return str ? str.toUpperCase() : ''}
src/mod2.js
// 监听全局对象 window 的事件window.onload = function () {console.log('loaded!!!!!')}
index.js
// 调用 mod1 和 mod2import { toUpperCase } from './mod1'import './mod2'const result = toUpperCase('abc')console.log(result)
2、编写 webpack 配置
const path = require('path')module.exports = {entry: './src/index.js',output: {path: path.join(__dirname, 'dist'),filename: 'bundle.js'},// 关闭 devtool 以提高打包代码的可阅读性(否则会用 eval 处理代码)devtool: false,mode: 'development'}
3、执行打包
观察后可发现:在 index.js 中被导入的 mod1.js 的两个函数,只有 toUpperCase 被使用了,但最后 mod1.js 的所有代码都被打包了:
4、开始优化:配置 optimization.usedExports,让 webpack 查找和标记未使用的导出内容
// ...module.exports = {// ...// 打包优化选项optimization: {// 是否删除未被使用到的导出内容usedExports: true,},}
配置后打包,发现打包后的代码中对未使用的 trim函数做了标记:

5、此时再配置 optimization.minimize,就能达到压缩代码并删除已被标记函数的效果
// ...module.exports = {// ...// 打包优化选项optimization: {// 是否删除未被使用到的导出内容usedExports: true,// 是否开启代码压缩minimize: true,},}
6、另外,如 mod2.js 这种内部未导出任何内容、但含有操作全局对象的代码,也能被正确打包
这种代码被称为 “有副作用” 的代码,它们不应该进行摇树优化,否则会破坏程序正常运行。
可通过在 package.json 中设置一个 sideEffects 属性来告诉 webpack 代码是否有副作用,如有副作用则应谨慎处理。该属性可设的值:
- true - 默认值,代表所有文件都有副作用。优化时应采用谨慎策略
- false - 代表所有文件都无副作用。可安全优化
- 文件路径数组 - 代表数组中的文件有副作用
做个小测试:如果在 package.json 中设置 sideEffects 为 false:
{"sideEffects": false}
打包后发现 mod2.js 的代码都被删除了,这肯定是不正确的!!!
因此,对于当前程序来说:
mod1.js是无副作用的mod2.js是有副作用的
应该如下设置才能保证 mod2.js 代码被保留:
{"sideEffects": ["./src/mod2.js"]}
另外值得注意的是,通过 import '../main.css' 方式引用的样式文件也属于有副作用的代码,应正确设置,避免被错误优化。
作用域提升
通过”提升“被导入的模块代码的作用域,达到提升代码性能和减少代码量的效果
核心内容:
- 作用域提升的概念和作用
- 使用插件完成作用域提升
众所周知,JavaScript 存在 ”函数提升“ 和 ”变量提升“ 的概念,即 JavaScript 运行时会把函数和变量声明提升到当前作用域的顶部。
而 webpack 中的 ”作用域提升“ 功能也与之类似,它会把被引用的 js 文件合并且放置到引入 js 文件的顶部。
作用域提升优化后的效果:
- 减少打包后的代码量
- 减少代码运行时占用的内存
- 提升代码运行的速度
步骤大纲
- 创建新目录,并编写三个依赖关系为 index -> a -> b 的模块
- 编写 webpack 配置文件,打包后观察结果模块的结构
- 启用
optimization.concatenateModules: true优化项,打包后观察结果
具体步骤:
1、创建新项目,编写如下文件
src/b.js
export function printMessage(str) {console.log(`打印消息:${str}`)}
src/a.js
import { printMessage } from './b'export function printA() {printMessage("这是a模块中打印的消息")}
src/index.js
import { printA } from './a'printA()
2、编写打包配置文件 webpack.config.js
const path = require('path')module.exports = {mode: 'development',devtool: false,entry: './src/index.js',output: {path: path.join(__dirname, 'dist'),filename: 'app.bundle.js'},}
此时还未使用作用域提升进行优化,因此打包后的代码中各模块都还是独立的:
3、配置优化选项 optimization.concatenateModules
// ...module.exports = {// ...optimization: {concatenateModules: true}}
再次打包后:a、b 模块的代码都以内联的方式直接放到了 index 前面:
效果验证:
- 减少代码量 —> 确实变少了
- 减少内存占用 —> 因为代码被简化后包装函数减少了,降低了本来要分配给这些函数的内存
- 提升运行速度 —> 因为不用通过 webpack_require 调用模块了
【重要说明】
当然,webpack 也不会一股脑把所有模块都堆砌到一个模块中,比如以下情况下模块们依然会被一一拆分开:
- 使用非 ES6 的模块机制
- 使用 import() 异步导入
添加 CSS 前缀
自动为 css 样式属性添加跟浏览器相关的前缀,提高样式代码兼容性
核心内容:
- 安装和配置 postcss-loader ,通过 PostCSS 添加样式前缀
步骤大纲
- 创建新目录,编写一些在多种浏览器上可能存在兼容性写法的样式(如 flex 相关样式)
- 创建入口 js 并引入样式
- 安装常用处理 css 的一些 loader,及
postcss-loader和相关依赖包 - 编写 webpack 配置,使用
postcss-loader作为第一道处理 css 的 loader - 打包并观察结果
具体步骤
1、新建项目,编写如下文件
src/main.css
.test {display: flex;flex-direction: column;}
src/index.js
import './main.css'
2、安装css-loader, mini-css-extract-plugin, postcss-loader 及相关依赖包
npm i css-loader mini-css-extract-plugin -D
npm i postcss-loader postcss postcss-preset-env -D
3、配置 webpack
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const path = require('path')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'app.bundle.js'
},
module: {
rules: [
{
test: /\.css$/i,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
// 用 PostCSS 处理样式文件
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
["postcss-preset-env", {
//兼容到浏览器版本的的倒数第二个版本
browsers: ['last 2 versions']
}]
],
},
}
}
]
}
]
},
plugins: [
new MiniCssExtractPlugin()
]
}
配置后打包,能看到某些样式已添加前缀:
.test {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
}
压缩 CSS
让打包输入的 css 文件进行压缩处理,减少文件体积
核心内容:
- 通过插件
css-minimizer-webpack-plugin来压缩样式代码
步骤大纲
- 安装
css-minimizer-webpack-plugin - 使用
optimization.minimizer优化项来指定压缩 css 的插件 - 打包后观察结果
具体步骤
1、在之前案例基础上,安装 css-minimizer-webpack-plugin
npm i css-minimizer-webpack-plugin -D
2、配置 webpack
// ...
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
module.exports = {
// ...
optimization: {
minimize: true,
minimizer: [
// 表示保留当前已有的压缩器
`...`,
// 添加 CSS 压缩器
new CssMinimizerPlugin()
]
}
}
配置后打包,能看到样式文件已被压缩。
抽取公共模块
将多个入口代码中都引用的 js 模块,抽取成独立文件,达到拆分公共代码的目的(有利于文件缓存)
核心内容:
- 了解为什么要抽取公共模块
- 使用 optimization.spliatChunks 配置公共代码抽取
不抽取前的问题:
如果一个项目含有多个入口,且它们又都引入了一些相同的模块,则打包后各入口对应的结果文件中都会包含相同的代码,产生冗余问题:
解决方法
在 webpack 4 之前,一般使用 CommonsChunkPlugin 插件来解决该问题,不过它的功能较弱。
从 webpack 4 开始它被 SplitChunksPlugin 插件取代,且默认就已启用,在不做任何配置的情况下,如当前代码文件符合下列条件,就会自动被拆分:
- 当一个 chunk 被其他多个 chunk 共享,或模块来自于 node_modules 文件夹
- 当一个 chunk 体积在进行 min + gz 之前的尺寸 > 20kb
- 当一个 chunk 在按需加载时最大的并行请求数 <= 30
- 当加载初始页面时的最大并发请求数 <= 30
且 SplitChunksPlugin 的使用方式和其他插件也不同,它通过 optimization.splitChunks 来配置。
步骤大纲
- 创建新目录,编写一个公用工具模块
- 编写两个入口 js 文件,它们都引用了上面的工具模块
- 安装 jquery 并在上面其中一个模块中调用
- 编写 webpack 配置,处理多入口,并通过 optimization.splitChunks 配置项设置分别对工具模块、npm 安装的模块进行抽取
- 打包并观察结果
具体步骤
1、创建新项目,编写以下文件
公共模块:src/common/index.js
export function toUpperCase(str) {
return str ? str.toUpperCase() : ''
}
export function formatDate(timestamp) {
const date = new Date(timestamp)
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
return `${year}年${month}月${day}日`
}
页面入口1:src/index1.js
// 用到了 jquery,请用 npm 安装依赖包
import jquery from 'jquery'
import { toUpperCase } from './common'
const text = toUpperCase('hello,world')
console.log(text)
// 调用 node_moudules 中的模块
console.log(jquery('.btn'))
页面入口2:src/index2.js
import { formatDate } from './common'
const date = formatDate(Date.now())
console.log(date)
2、配置 webpack
const path = require('path')
module.exports = {
mode: 'development',
devtool: false,
entry: {
app1: './src/index1.js',
app2: './src/index2.js',
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].bundle.js'
},
optimization: {
splitChunks: {
cacheGroups: {
// 抽取我们自己编写的公共代码
commons: {
minChunks: 2,
chunks: 'initial',
name: 'commons',
// 由于当前我们的 common 模块代码较少(小于20K),
// 所以默认不会生成独立的 chunk 文件的,而使用 enforce 可强制生成
enforce: true,
},
// 抽取通过 npm 安装的 node_modules 下的模块
vendors: {
ctest: /[\\/]node_modules[\\/]/,
chunks: 'all',
name: 'vendors',
},
},
}
}
}
执行打包后观察结果文件:
使用动态导入按需加载
使用 webpack 的按需导入函数作模块的按需加载,并为这些模块生成独立的 js 文件
核心内容:
- 按需导入函数
import() - 通过 webpack 为按需导入的模块生成独立的 js 文件
与 ES6 import 关键字不同,import() 是 webpack 提供的函数,用于在代码执行到当前调用的地方时再去加载指定的模块文件,即按需加载。按需加载的文件将被 webpack 打包成独立的文件。
步骤大纲
- 编写一个简单的js模块
- 在入口js中通过
import()函数加载以上 js 模块 - 打包并观察结构,看是否为该模块生成了独立文件
具体步骤
1、在之前章节代码的基础上,编写如下代码
- 新增:
src/dynamic/dyna1.js - 修改:
src/index2.js
src/dynamic/dyna1.js
console.log('>>>>>Test Daynamic Module')
src/index2.js
import { formatDate } from './common'
const date = formatDate(Date.now())
console.log(date)
// 使用 import() 函数动态导入
import('./dynamic/dyna1')
2、执行打包,可以看到生成了 dyna1.js 对应的文件

之前章节讲到,webpack 的默认抽取规则为:“当 chunk 在按需加载时最大的并行请求数 <= 30” ,就会自动生成独立文件:
- 当前示例代码中只有一个地方通过
import()加载dyna1.js,满足该条件,所以生成了独立文件 - 如果代码中存在 30 次以上的
import()加载dyna1.js,则就不会生成独立文件
如想改变以上阈值,可通过 optimization.splitChunks 下的配置项:
maxAsyncRequests- 最大异步请求数maxInitialRequests- 最大初始请求数
忽略对无需解析文件的处理
让 webpack 忽略解析通过 npm 安装的模块,提高构建速度
核心内容:
- 提升原理
- webpack 配置:
module.noParse的配置
解析和不解析的区别:
- 解析:读懂内容
- 不解析:无脑 CV
默认情况下 webpack 会对任意引用到的模块进行解析,当解析较大型第三方库的代码时会消耗很多时间。
其实大多数从 npm 下载的包都已自行做了构建处理,无需再次进行解析。
通过 webpack 配置项 noParse ,可以让 webpack 忽略对指定文件的解析。该值是一个用来匹配文件路径的正则。(需要注意的是:被忽略的文件中不能使用任何模块化机制)。
步骤大纲
- 查看当前项目的打包消耗时间
- 配置 webpack 的
noParse,忽略对第三方包的解析(比如jquery) - 再次打包,观察消耗时间
具体步骤
1、在之前的案例中引入了 jquery,我们在打包后查看当前花费的时间

2、配置 webpack
// ...
module.exports = {
// ...
module: {
//不解析 jquery 包
noParse: /jquery/,
}
}
再次执行打包,查看消耗时间:

排除无需打包的文件
将不需要的文件排除在打包结果之外,仅打包所需的文件,减少打包后的文件大小
(一般用于打包第三方提供的库时,会遇到这样的场景)
核心内容:
- 场景说明
- 通过 webpack 插件
IgnorePlugin实现该优化
成熟的第三方库一般为满足尽可能多的用户,会提供比较细致完善的功能。比如:很多涉及多语言的库,就会提供很多国家的语言文件包,且默认将它们全部打包。但有的开发者只需一两种语言就够了,如果采用默认打包方式就会让打包后的文件很大。
webpack 内置的 IgnorePlugin 可用来指定哪些文件无需打包。
下面以 moment 库为例。
步骤大纲
- 为项目安装
moment,并在代码中使用moment - 打包并观察结果文件
- 本地安装
webpack包,并配置IgnorePlugin插件来忽略moment库中所有locale目录下的文件 - 打包并观察结果文件
- 单独引入所需的语言文件
- 打包并观察结果文件
具体步骤
1、创建新项目,并安装 moment
npm i moment -D
2、编写 src/index.js
// 导入 moment
import moment from 'moment'
// 设置语言
moment.locale('zh-cn')
// 调用 moemnt 来处理日期
const result = moment().endOf('day').fromNow()
console.log(result)
3、安装 webpack,并编写 webpack 配置
npm i webpack -D
webpack.config.js
const path = require('path')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'app.bundle.js'
},
}
配置后打包,在打包结果中能看到各种 moment 语言信息。
4、添加 IgnorePlugin 配置
// ...
const { IgnorePlugin } = require('webpack')
module.exports = {
// ...
plugins: [
new IgnorePlugin({
//要排除的模块名字
contextRegExp: /moment/,
//要排除的模块的路径
resourceRegExp: /\.\/locale/,
})
]
}
配置后打包,打包结果中一个语言信息都没了。
5、在 src/index.js 中通过 import 只导入中文语言
// 只导入中文语言包
import 'moment/locale/zh-cn'
再次打包,打包结果中只包含了中文语言信息。
并行构建
让原本单进程的构建过程,变成多进程并行构建,提高打包速度
核心内容:
- webpack 单进程的特性介绍
- 通过使用 HappyPack 插件实现多进程方式的构建
要解决的问题?
由于 Node.js 是单线程模型,所以 Webpack 只能一个个文件依次处理,不能同时进行。
当构建文件数量较多的中大型项目时,就会花费很长时间。
解决方案?
可使用插件 HappyPack ,让 Webpack 能同时处理多个任务。
它的原理:主进程创建多个子进程,将要处理的文件委托这些子进程进行并发处理,处理完成后再把结果发回主进程进行合并。这能减少总的构建时间。
步骤大纲
- 在项目中安装
happypack - 在配置文件中创建 HappyPack 进程池
- 在配置文件中创建 HappyPack实例,关联进程池,并指定要通过 HappyPack 代理处理的 loader
- 打包
具体步骤
1、在任意 webpack 项目中,安装 happypack (本案例基于之前 babal-loader 示例代码)
npm i happypack -D
2、在 webpack.config.js 中创建 HappyPack 进程池
const HappyPack = require('happypack')
const os = require('os')
// HappyPack 进程池
const threadPool = HappyPack.ThreadPool({
// 根据当前计算机的 CUP 数量来决定进程池的进程数量
size: os.cpus().length
})
3、配置 HappyPack 插件来委托运行原先的 babal-loader
// ...
module.exports = {
// ...
module: {
rules: [
// ...
{
// 处理 js 文件
test: /\.js$/,
// 记得一定要排除掉通过包管理器安装的代码目录,否则转换会非常慢
exclude: /node_modules/,
// 配置 babel-loader (通过 happypack 运行)
use: 'happypack/loader?id=happyBabel'
}
]
},
// 插件
plugins: [
// ...
new HappyPack({
//每一个进程需要一个独一无二的id
id: 'happyBabel',
//需要使用happyhapck执行的loader
loaders: [{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-transform-runtime']
}
}],
threadPool,
})
]
}
配置后打包,可看到如下信息则说明 HappyPack 已生效:

重要说明:
- 在文件较少时,使用 HappyPack 其实反而会增加构建时间,因为创建和管理额外的进程也需要时间。建议在文件较多的项目中使用,才能体现出整体性价比
- 另外 HappyPack 目前还未完全支持 webpack 5,有些 loader 会运行失败,使用时需要注意
利用浏览器缓存文件
通过为打包生成的文件指定特定文件名,让浏览器能缓存没有变化的文件
核心内容:
- 浏览器对文件的缓存机制
- 配置 webpack,生成特定文件名的结果文件
URL对缓存的影响:
http://www.abc.com/test.js (使用缓存)
http://www.abc.com/test.js?v=时间戳 (不使用缓存)
http://www.abc.com/test.xxxxx.js (当 xxxxx 变动时,不使用缓存)
浏览器通过内存或磁盘对文件进行缓存:
具体步骤
1、以下面的 webpack.config.js 配置为例,打包后生成的文件名永远是 bundle.js
const path = require('path')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
}
}
当页面中引入该 js 文件时,浏览器后续会会尝试从缓存中加载该文件,而不是从后端请求。
但如果发布了新版本,而打包文件仍为 bundle.js ,这时就会有问题:用户浏览器不一定能重新从我们的服务器获取最新代码。
2、可借助 webpack 的 hash 生成功能,为文件名携带唯一的 hash 值
// ...
module.exports = {
// ...
output: {
// ..
// [hash] 表示一个占位符,用 hash 内容填充
filename: 'bundle.[fullhash].js'
}
}
打包后的文件名:

可用的 hash 生成器有3个:
**fullhash**- 只要有一个文件发生改动,则会为所有打包文件的名称生成新的 hash,即所有缓存的文件都会失效【工程级别缓存】**chunkhash**- 一个入口所关联的文件内容发生改动,则为该入口所依赖的所有文件生成新 hash【入口级别缓存】**contenthash**- 只为改动过的模块生成新 hash【文件级别缓存】
对打包结果进行分析
通过使用分析工具,生成构建分析报告,帮助找出可优化的地方
核心内容:
- 优化的思想
- 分析工具的使用
任何优化操作都不会是盲目开展的,而是遵循一些基本规则:
- 首先,要知道问题出在哪里,即要优化什么
- 其次,针对可以优化的点,选择相应的优化方案
- 最后,要衡量优化前后对项目产生的影响
几种可视化分析工具
webpack-bundle-analyzer 插件- webpack visualizer
- webpack analyse
具体步骤
工具一:webpack-bundle-analyzer 插件的使用
1、在项目中安装 webpack-bundle-analyzer
npm i webpack-bundle-analyzer --save-dev
2、配置 webpack
// ...
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = {
// ...
plugins: [
// ...
// 开启 BundleAnalyzerPlugin
new BundleAnalyzerPlugin(),
],
}
执行打包后,会自动打开打包结果的图形化分析网页,查看使用到的模块及依赖关系:
工具二:webpack visualizer 的使用
1、生成统计文件 stats.json
webpack --profile --json > stats.json
2、 打开工具主页 http://chrisbateman.github.io/webpack-visualizer/,并上传 stats.json

Webpack Visualizer 能以可视化方式展示您的 webpack 捆绑包的分析结果,可以清晰的看到哪些模块正在占用空间、哪些可能是重复的。
工具三:webpack analyse 的使用
1、生成 stats.json
2、从 GitHub 克隆代码: https://github.com/webpack/analyse
克隆完代码后,安装 npm 依赖包,然后执行 npm run dev 启动服务。
3、上传 stats.json
Webpack Analyse 提供了对包的更全面的分析,能绘制项目中所有依赖模块的图形,对于依赖关系较少的项目非常有用。
(但依赖关系多的话,看起来真的非常费力!)
更多工具参考
预拉取和预加载
使用import()进行模块懒加载时,可通过在网络空闲时预先下载 chunk 代码,来实现用户体验的提升
核心内容:
- 使用懒加载时的问题
- 解决方案:预拉取和预加载的使用
在使用模块懒加载的情况下,只有当执行到懒加载的代码时才会开始下载和执行。这有可能会导致出现用户的交互失败或延迟的情况。
解决该问题的方式是:使用 webpack 提供的预拉取 (prefetch) 和预加载 (preload) 。
用法是在 import() 函数中使用”魔法注释“:
/ webpackPrefetch: true /
/ webpackPreload: true /
const Home = () => import(/* webpackPrefetch: true */ './...../Home.vue')
preload 和 prefetch 的区别:
- preload 文件会和父文件一起同时加载;而 prefetch 文件会在父文件加载后才开始加载
- preload 文件会立即下载;而 prefetch 文件只在浏览器闲置时才下载
- 浏览器支持程度不同
具体步骤
1、在通过 import() 函数进行懒加载的代码上,添加魔法注释:
const Home = () => import(
/* webpackPrefetch: true */
'@/views/Home/index.vue'
)
const About = () => import(
/* webpackPreload: true */
'@/views/About/index.vue'
)
2、打包后,index.html 中的 <head> 下会添加类似如下标签
<link rel="prefetch" as="script" href="./src_views_Home_index_vue.app.bundle.js">
<link rel="prefetch" as="script" href="./src_views_About_index_vue.app.bundle.js">
