文章目录
-
- [1. 性能优化-分包](#1. 性能优化-分包)
- [2. 动态导入](#2. 动态导入)
- [3. 自定义分包](#3. 自定义分包)
- [4. Prefetch和Preload](#4. Prefetch和Preload)
- [5. CDN加载配置](#5. CDN加载配置)
- [6. CSS的提取](#6. CSS的提取)
- [7. terser压缩](#7. terser压缩)
-
- [7.1 Terser在webpack中配置](#7.1 Terser在webpack中配置)
- [7.2 css压缩](#7.2 css压缩)
- [8. Tree Shaking 消除未使用的代码](#8. Tree Shaking 消除未使用的代码)
-
- [8.1 usedExports 配置](#8.1 usedExports 配置)
- [8.2 sideEffects配置](#8.2 sideEffects配置)
- [8.3 CSS实现Tree Shaking](#8.3 CSS实现Tree Shaking)
- [9. Scope Hoisting作用域提升](#9. Scope Hoisting作用域提升)
- [10 webpack对文件进行压缩](#10 webpack对文件进行压缩)
webpack性能优化
-
webpack的性能优化较多,可以对其进行分成两类:
- 打包后的结果,上线时的性能优化。(比如
分包处理、减小包体积、CDN服务器
等) - 优化打包速度,开发或者构建时优化打包速度。(比如
exclude、cache-loader
等)
- 打包后的结果,上线时的性能优化。(比如
-
但是在大多数情况下webpack都帮我们做好了该有的性能优化:
- 比如
配置mode为production或者development时
,默认webpack的配置信息; - 但是我们也可以针对性的进行自己的项目优化;
- 比如
1. 性能优化-分包
-
主要的目的是将代码分离到不同的bundle中,之后我们可以按需加载 ,或者并行加载这些文件;
-
比如默认情况下,所有的JavaScript代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页的加载速度;
-
代码分离可以分出更小的bundle,以及控制资源加载优先级,提供代码的加载性能;
-
Webpack中常用的代码分离有三种:
入口起点
:使用entry配置手动分离代码;防止重复
:使用Entry Dependencies或者SplitChunksPlugin去重和分离代码;动态导入
:通过模块的内联函数调用来分离代码;
-
多入口起点
上面代码就是多入口,打包出来后可以看到代码进行了分离
-
但是当上面的的
index.js
和main.js
都依赖相同的库axios
这时打包就会把axios
打包到每一次文件中,显然是不符合要求的。 -
这时我们就需要让他们共享
axios
依赖
javascript
// entry: './src/index.js',
// 对象配置多入口
// 打包时需要注意出口
entry: {
index: { import: './src/index.js', dependOn:'shared'},
main: { import: './src/main.js', dependOn: 'shared' },
// 配置共享打包依赖
shared: ['axios']
},
output: {
path: path.resolve(__dirname, './build'),
// filename: 'bundle.js',
// 出口配置动态获取name
filename: "[name].bundle.js",
clean: true
},
- 打包后
2. 动态导入
-
在ECMAScript语法中的导入是通过
import()
语法来完成 -
例如在首屏渲染时,加载所有的路由会很慢,包也会很大
- 我们希望在代码运行过程中来加载它(比如判断一个条件成立时加载);
- 因为我们并不确定这个模块中的代码一定会用到,所以最好拆分成一个独立的js文件;
- 这样可以保证不用到该内容时,浏览器不需要加载和处理该文件的js代码;
- 这个时候我们就可以使用动态导入;
-
- 新建router路由
- 新建router路由
- 在
main.js
中用import
导入
javascript
const btn1 = document.createElement('button')
const btn2 = document.createElement('button')
btn1.textContent = '关于路由'
btn2.textContent = '主页面路由'
document.body.append(btn1)
document.body.append(btn2)
btn1.onclick = function () {
import('./router/about')
}
btn2.onclick = function () {
import('./router/home.js')
}
- 打包后
- 这时我们就可以按需加载这些文件了,并且首屏加载时,包的体积也会很小,速度大大提高
- 在webpack中,也可以通过
动态导入获取到一个对象或者调用方法
; - 真正导出的内容,在该对象的
default属性
中,所以我们需要做一个简单的解构;
- 上面代码打包后的名字
src_router_home_js_bundle.js
,我们不能见名知意 - 因此动态导入的文件也可以命名:
- 因为动态导入通常是一定会打包成独立的文件的,所以并不会在
cacheGroups
中进行配置; - 那么它的命名我们通常会在
output中,通过 chunkFilename
属性来命名;
- 因为动态导入通常是一定会打包成独立的文件的,所以并不会在
javascript
output: {
clean: true,
path: path.resolve(__dirname, './build'),
filename: "[name].bundle.js",
// 分包进行命名
chunkFilename: "[name]_chunk.js"
},
- 但是,也会发现默认情况下我们获取到的 [name] 是和id的名称保持一致的
- 我们希望修改name的值,可以通过
magic comments(魔法注释)
的方式;
javascript
btn1.onclick = function () {
import(/* webpackChunkName: "about" */'./router/about').then(res=>{
res.about()
})
}
- 同时import使用最多的也是路由懒加载
javascript
const btn2 = document.createElement('button')
btn2.textContent = '主页面路由'
const component = document.createElement('div')
component.innerHTML='hello component'
btn2.onclick = function () {
import('./router/home.js').then(() => {
document.body.appendChild(component)
})
}
document.body.append(btn2)
3. 自定义分包
- 当直接引入第三方库时,并不会对第三方进行分包,因此就需要
splitChunk
- splitChunk可以自定义分包,它底层是使用
SplitChunksPlugin
来实现的:- 该插件
webpack已经默认安装和集成
, 只需要提供SplitChunksPlugin相关的配置信息即可;
- 该插件
- Webpack提供了SplitChunksPlugin默认的配置,同时也可以手动来修改它的配置:
- 比如默认配置中,chunks仅仅针对于异步(
async
)请求,我们也可以设置为all
;
- 比如默认配置中,chunks仅仅针对于异步(
javascript
// chunks 默认值是async ,只对异步进行分包
// 可以设置成all进行分包
optimization: {
splitChunks: {
chunks: 'all'
}
},
- axios分包后的代码
注意
上面的分包会把所有的第三方库都会分到一个包中- 自定义配置解析
- Chunks:
- 默认值是async
- all表示对同步和异步代码都进行处理
- minSize:
- 拆分包的大小, 至少为minSize;
- 如果一个包拆分出来达不到minSize,那么这个包就不会拆分;
- maxSize:
- 将大于maxSize的包,拆分为不小于minSize的包;
- cacheGroups:
- 用于对拆分的包就行分组,比如一个lodash在拆分之后,并不会立即打包,而是会等到有没有其他符合规则的包一起来打包;
- test属性:匹配符合规则的包;
- name属性:拆分包的name属性;
- filename属性:拆分包的名称,可以自己使用placeholder属性;
- Chunks:
- 如果进行继续拆分可以参考webpack官网
javascript
// chunks 默认值是async ,只对异步进行分包
// 可以设置成all进行分包
optimization: {
splitChunks: {
chunks: "all",
// 当一个包大于指定的大小时, 继续进行拆包
// maxSize: 20000,
minSize: 20000, // 将包拆分成不小于minSize的包 这个默认值是20kb
// 自己对需要进行拆包的内容进行分包
cacheGroups: {
utils: {
test: /utils/,
filename: "[id]_utils.js"
},
vendors: {
// /node_modules/
// window上面 /\
// mac上面 /
test: /[\\/]node_modules[\\/]/,
filename: "[id]_vendors.js"
}
}
},
},
- 同时因为环境不同生成的打包
前缀id
也不同
- 因此
optimization.chunkIds
可以配置- optimization.chunkIds配置用于告知webpack模块的id采用什么算法生成。
- 有三个比较常见的值:
natural
:按照数字的顺序使用id;named
:development下的默认值,一个可读的名称的id;deterministic
:确定性的,在不同的编译中不变的短数字id
注意
在webpack4中是没有这个值的;- 那个时候如果使用natural,那么在一些编译发生变化时,就会有问题;
- 开发过程中,我们推荐使用
named;
- 打包过程中,我们推荐使用
deterministic
;
javascript
optimization: {
// 设置生成的chunkId的算法
// development: named
// production: deterministic(确定性)
// webpack4中使用: natural
chunkIds: 'deterministic',
// runtime的代码是否抽取到单独的包中(早Vue2脚手架中)
// 主要是对模块进行解析、加载、模块信息相关的代码
runtimeChunk: {
name: "runtime"
},
}
4. Prefetch和Preload
- webpack v4.6.0+ 增加了对预获取和预加载的支持。
- 在声明 import 时,使用下面这些内置指令,来告知浏览器:
- prefetch(预获取):将来某些导航下可能需要的资源
- preload(预加载):当前导航下可能需要资源
javascript
btn2.onclick = function () {
import(
/* webpackChunkName: "about" */
/* webpackPreload:true */
'./router/home.js').then(() => {
document.body.appendChild(component)
})
}
- 与 prefetch 指令相比,preload 指令有许多不同之处:
- preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
- preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
- preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
5. CDN加载配置
-
CDN称之为内容分发网络
-
在开发中,我们使用CDN主要是两种方式:
- 打包的所有静态资源,放到CDN服务器,用户所有资源都是通过CDN服务器加载的;
- 一些第三方资源放到CDN服务器上
-
打包的所有静态资源,放到CDN服务器
javascript
output: {
publicPath: 'http://btn12or.com/'
},
- 打包后
javascript
<script defer src="http://btn12or.com/runtime.bundle.js"></script>
<script defer src="http://btn12or.com/291_vendors.js"></script>
<script defer src="http://btn12or.com/main.bundle.js"></script>
- 排除打包,第三方资源放到CDN服务器上加载
- 配置排除打包
javascript
// 排除打包 ,注意这里的key与value要和导入的保持一致
// key 是导入使用排除的包
// value 是CDN提供的名字
externals:{
axios:'axios'
},
- 在html模块中,加入CDN服务器地址:
javascript
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.6/axios.min.js"></script>
6. CSS的提取
安装 npm i style-loader css-loader -D
- 配置
javascript
{
test: /\.css$/,
use: [
'style-loader', // 开发环境使用
// MiniCssExtractPlugin.loader, // 生产环境
'css-loader'
]
}
- 安装 npm install mini-css-extract-plugin -D 用于提取css
注意
该插件需要在webpack4+以上使用注意
默认css是直接内联到html中的
- 引用css , 并导入插件与配置
javascript
// 0. main.js 中引入
import './css/style.css';
// 1. 导入
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// 2. 配置loader
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader', // 开发环境使用
MiniCssExtractPlugin.loader, // 生产环境
'css-loader'
]
}
]
},
// 3. 配置plugins
plugins: [
// css / [name].css 文件夹与文件名
new MiniCssExtractPlugin({
filename: 'css/[name].css',
// 单独导入的css进行分包
chunkFilename: 'css/[name]_chunk.css'
})
]
7. terser压缩
Terser
可以帮助我们压缩、丑化我们的代码,让我们的bundle变得更小- 早期使用
uglify-js
来压缩、丑化我们的JavaScript代码,但是目前已经不再维护,并且不支持ES6+的语法; Terser是从 uglify-es fork
过来的,并且保留它原来的大部分API以及适配 uglify-es和uglify-js@3
等;- Terser和babel都是一个独立的工具,所以它可以单独安装:
- 全局安装
npm install terser -g
- 局部安装
npm install terser -D
- 命令行使用Terser
命令 1.
terser [input files] [options]
举例 2.
npx terser qa.js -o qa.min.js -c -m
- 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,是否保持原来的函数名称;
- 具体配置参考属性
javascript
npx terser qa.js -o qa.min.js -c arrows,arguments=true,dead_code -m
toplevel=true,keep_classnames=true,keep_fnames=true
7.1 Terser在webpack中配置
- 真实开发中,不需要手动的通过terser来处理代码,直接通过webpack来处理
注意
在webpack中有一个minimizer
属性,在production模式下,默认就是使用TerserPlugin来处理我们的代码的;- 同时自己也可以自己来创建TerserPlugin的实例,并且覆盖相关的配置;
- 具体配置
- 在
development
模式下需要打开minimize
,让其对我们的代码进行压缩(默认production模式下已经打开了) - 导入
TerserPlugin
插件
- 在
javascript
const TerserPlugin = require('terser-webpack-plugin')
- 在minimizer创建一个TerserPlugin:
extractComments
:默认值为true,表示会将注释抽取到一个单独的文件中;在开发中,我们不希望保留这个注释时,可以设置为false;parallel
:使用多进程并发运行提高构建的速度,默认值是true- 并发运行的默认数量: os.cpus().length - 1;
- 我们也可以设置自己的个数,但是使用默认值即可;
terserOptions
:设置我们的terser相关的配置- compress:设置压缩相关的选项;
- mangle:设置丑化相关的选项,可以直接设置为true;
- toplevel:顶层变量是否进行转换
- keep_classnames:保留类的名称;
- keep_fnames:保留函数的名称;
- 具体配置参考webpack官网
javascript
// 1. webpack 中引用插件
const TerserPlugin = require('terser-webpack-plugin')
// 配置terserPlugin
optimization: {
// 代码优化: TerserPlugin => 让代码更加简单与压缩 => Terser
minimize: true, // minimize 在development 模式下必须手动指定为true ,在production模式下,webpack自动设置为true
minimizer: [
// JS代码简化
new TerserPlugin({
extractComments: false, // 第三方注释是否进行抽取
terserOptions:{
mangle:true,
toplevel:true
}
})
// CSS代码简化
]
},
7.2 css压缩
- CSS压缩通常是去除无用的空格等,因为很难去修改选择器、属性的名称、值等
- 安装
css-minimizer-webpack-plugin
插件
npm i css-minimizer-webpack-plugin
javascript
// 1. webpack 中引用插件
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
// 2. 使用
new CssMinimizerPlugin({
parallel:true
}),
8. Tree Shaking 消除未使用的代码
- 用于
消除未调用的代码
(纯函数无副作用
,可以放心的消除,这也是为什么要求我们在进行函数式编程时,尽量使用纯函数的原因之一) - 在webpack4正式扩展了这个能力,并且
通过 package.json的 sideEffects属性作为标记
,告知webpack在编译时,哪里文件可以安全的删除掉; - webpack5中,也提供了对部分CommonJS的tree shaking的支持 参考链接
- webpack实现Tree Shaking
usedExports
:通过标记某些函数是否被使用,之后通过Terser来进行优化的;sideEffects
:跳过整个模块/文件,直接查看该文件是否有副作用;
8.1 usedExports 配置
-
将mode设置为development模式:
- 为了可以看到 usedExports带来的效果,我们需要设置为 development 模式
- 因为在 production 模式下,webpack默认的一些优化会带来很大的影响。
-
设置usedExports为true和false对比打包后的代码:
-
源代码引用
-
usedExports为fasle
-
usedExports为true
-
在usedExports设置为true时,会有一段注释:
unused harmony export mul
-
告知Terser在优化时,可以删除掉这段代码
-
-
把minimize也设置true :mul函数有被移除掉
- usedExports设置为false时,mul函数没有被移除掉;
-
所以,usedExports实现Tree Shaking是结合Terser来完成的。
8.2 sideEffects配置
- sideEffects用于告知webpack compiler哪些模块时有副作用的:
- 副作用的意思是这里面的代码有执行一些特殊的任务,不能仅仅通过export来判断这段代码的意义;
- 在
package.json中设置sideEffects
的值:- 将
sideEffects设置为false
,就是告知webpack可以安全的删除未用到的exports;
- 将
- 源副作用代码
- 设置为true
- sideEffects设置为false 删除所有副作用代码
注意这里不管引入的是否是全局要用的代码还是css文件
只要变量未被接受都会被删除- 比如引入的css文件我们希望保留,可以设置为数组
- 源css文件
- 源css文件
- 打包后的文件
8.3 CSS实现Tree Shaking
- CSS的Tree Shaking需要借助于一些其他的插件
- 安装
npm install purgecss-webpack-plugin -D
- 配置PurgeCss
paths
:表示要检测哪些目录下的内容需要被分析,这里使用glob查找文件夹;- 默认情况下,Purgecss会将我们的html标签的样式移除掉,如果我们希望保留,可以添加一个safelist的属性;
javascript
// 1. 导入
const path = require('path')
const glob = require('glob')
const { PurgeCSSPlugin } = require('purgecss-webpack-plugin')
// 2.配置插件
new PurgeCSSPlugin({
// glob node提供 查找src文件下的所有文件夹
// nodir: true 是查找所有的文件
paths: glob.sync(`${path.resolve(__dirname, '../src')}/**/*`, { nodir: true }),
// 添加白名单,看哪些不需要删除,默认标签选择器是不删除的
safelist: function () {
return {
standard: ["body"]
}
}
})
- 打包后文件对比
9. Scope Hoisting作用域提升
- 默认情况下webpack打包会有很多的函数作用域,包括一些(比如最外层的)
IIFE
:- 无论是从最开始的代码运行,还是加载一个模块,都需要执行一系列的函数;
- Scope Hoisting可以将函数合并到一个模块中来运行
- 在production模式下,默认这个模块就会启用; 在development模式下,需要手动打开该模块
- webpack官网参考
javascript
// 1. 导入
const webpack = require('webpack');
// 2. 配置plugins
new webpack.optimize.ModuleConcatenationPlugin()
10 webpack对文件进行压缩
- 安装CompressionPlugin:
npm install compression-webpack-plugin -D
javascript
const CompressionPlugin = require("compression-webpack-plugin")
// 对打包后的文件(js/css)进行压缩
new CompressionPlugin({
test: /\.(js|css)$/,
algorithm: 'gzip', // 压缩算法
minRatio:0.7, // 压缩比例
threshold:50 // 设置文件从多大开始压缩
})
- 具体配置参考webpack
- html压缩借助
HtmlWebpackPlugin
插件
javascript
new HtmlWebpackPlugin({
template: './index.html',
// 自定义html压缩
minify: isProdution ? {
removeComments: true
} : false
}),
- inject:设置打包的资源插入的位置
- cache:设置为true,只有当文件改变时,才会生成新的文件(默认值也是true)
- minify:默认会使用一个插件html-minifier-terser
参考文章 - Mr_RedStar
- coderwhy
- webpack官网