《webpack性能优化总结》
# 前言
当项目越来越大越来越复杂时,如何进行优化成为了必须解决的难题。webpack 官网也有针对性能优化给出了指南。详细内容可看 —— 《webpack指南》。
本文也对 webpack 性能优化做一下总结。webpack 性能优化可以从两大方面进行优化,一个是优化打包构建,一个是优化代码产出。下面针对这两个方面分别介绍。
# 优化打包构建
# 1.开启自动刷新
启用 Watch 模式。这意味着在初始构建之后,webpack 将继续监听任何已解析文件的更改。
webpack-dev-server 和 webpack-dev-middleware 里 Watch 模式默认开启。
// 开发环境
module.exports = {
watch: true, // 开启监听,默认为false
// 监听配置
watchOptions: {
ignored: /node_modules/,
// 监听到变化后等待 300ms 再执行,防止文件更新太快导致重新编译效率太高
aggregateTimeout: 300, // 默认为 300ms
// 轮询文件是否发生变化
poll: 1000 // 默认每隔 1000ms 询问一次
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 2.开启热更新
上面的自动刷新会刷新整个页面,如果想不刷新页面又要实现刷新,就需要开启热更新。
通过 HotModuleReplacementPlugin 插件可以实现。
// 开发环境
const webpack = require('webpack')
module.exports = {
mode: 'development',
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}
2
3
4
5
6
7
8
如果使用了webpack-dev-server,只需要配置 devServer.hot 即可。设置后 webpack 会默认引入 HotModuleReplacementPlugin。
module.exports = {
mode: 'development',
devServer: {
hot: true
}
}
2
3
4
5
6
# 3.优化 babel-loader
给 babel-loader 开启缓存 loader 的执行结果,并设置范围。
module.exports = {
module:{
rules: [
{
test: /\.js$/,
use: ['babel-loader?cacheDirectory'], // 开启缓存
include: path.resolve(__dirname, 'src'), // 明确范围
// exclude: path.resolve(__dirname, 'node_modules') // 排除范围,和 include 二选一即可
}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 4.设置 noParse
配置 module.noParse 忽略一些大型的 library 可以提高构建性能。
module.exports = {
module:{
noParse: /jquery|lodash/,// 不解析 jquery 和 |lodash
}
}
2
3
4
5
# 5.配置 IgnorePlugin
配置 IgnorePlugin 可以忽略某些内容不参与打包。
module.exports = {
plugins: [
new webpack.IgnorePlugin({
// 忽略 moment 的本地化内容
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/
})
]
}
2
3
4
5
6
7
8
9
# 6.使用 DLLPlugin 预编译
DLL(Dynamic Link Library) 文件为动态链接库文件,在 Windows 中,许多应用程序并不是一个完整的可执行文件,它们被分割成一些相对独立的动态链接库,即 DLL 文件,放置于系统中。当我们执行某一个程序时,相应的 DLL 文件就会被调用。
通常来说,我们的代码都可以至少简单区分成业务代码和第三方库。如果不做处理,每次构建时都需要把所有的代码重新构建一次,耗费大量的时间。然后大部分情况下,很多第三方库的代码并不会发生变更(除非是版本升级),这时就可以用到dll:把复用性较高的第三方模块打包到动态链接库中,在不升级这些库的情况下,动态库不需要重新打包,每次构建只重新打包业务代码。
webpack 提供了内置插件 DLLPlugin ,它跟 output.library
的选项相结合可以暴露出 全局 dll 函数。
配置:webpack.dll.js
module.exports = {
entry: {
react: ['react', 'react-dom']
},
output: {
filename: '[name].dll.js',
path: distPath,
library: '[name]_dll_[hash]' // 第三方库名,加上 _dll_ 避免全局变量冲突
},
plugins: [
new webpack.DllPlugin({
name: '[name]_dll_[hash]',
// 描述动态链接库的 manifest.json 文件输出时的文件名称
path: path.join(distPath, '[name].manifest.json')
})
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
使用:webpack.config.js
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
manifest: require(path.join(distPath, 'react.manifest.json'))
})
]
}
2
3
4
5
6
7
# 7.使用 HappyPack
使用 happypack 可以开启多进行打包,提高构建速度。
注意!创建子进程和子进程和主进程之间通信也是有开销的,如果项目很小也加上了 happypack,可能会编译的更慢!
const HappyPack = require('happypack');
// 线程池
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
module.exports = {
module: {
rules: [
{
test: /\.js$/,
// 把对js文件的处理转交给 id 为 babel的 Happypack 实例
use: ['happypack/loader?id=babel'],
include: path.resolve(__dirname, 'src')
},
{
test: /\.less$/,
use: 'happypack/loader?id=styles'
}
]
},
plugins: [
new HappyPack({
id: 'babel', // id 标识
threadPool: happyThreadPool, // 共享线程池
loader: ['babel-loader?cacheDirectory']
}),
new HappyPack({
id: 'styles', // id 标识
threadPool: happyThreadPool, // 共享线程池
loaders: [ 'style-loader', 'css-loader', 'less-loader' ]
})
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 8.使用 thread-loader
除了使用 Happypack 外,还可以使用 thread-loader ,把 thread-loader 放置在其它 loader 之前,那么放置在这个 loader 之后的 loader 就会在一个单独的 worker 池中运行。
在 worker 池中运行的 loader 是受到限制的。例如:
- 这些 loader 不能生成新的文件
- 这些 loader 不能使用自定义的 loader API(也就是说,不能通过插件来自定义)
- 这些 loader 无法获取 webpack 的配置
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve('src'),
use: [
"thread-loader",
// 耗时的 loader (例如 babel-loader)
"babel-loader"
]
}
]
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
thread-loader 和 Happypack 构建时间基本没什么差别,不过 thread-loader 配置起来为简单。
# 9.使用 ParallelUglifyPlugin
webpack 默认提供了 UglifyJsPlugin 来压缩JS代码,但是它使用的是单线程压缩代码,也就是说多个js文件需要被压缩,它需要一个个文件进行压缩。
ParallelUglifyPlugin 可以会开启多个子进程,把对多个文件压缩的工作分别给多个子进程去完成,但是每个子进程还是通过 UglifyJS 去压缩代码。
注意!开启多进程是有开销的,如果项目很小也开启了,可能会变得更慢!
// 生产环境
import ParallelUglifyPlugin from 'webpack-parallel-uglify-plugin';
module.exports = {
plugins: [
new ParallelUglifyPlugin({
uglifyJS: {
output: {
beautify: false, // 紧凑输出,没有空格
comments: false // 删除所有注释
},
compress: {
drop_console: true, // 删除所有 console 语句,可以兼容ie
warnings: false, // 删除 UglifyJS 在没有用到的代码时输出警告信息
collapse_vars: true, // 内嵌只使用了一次的变量,不做转换(如将 var x = 1; y = x, 转换成 y = 5)
/*
* 提取出现了多次但是没有定义成变量去引用的静态值,比如将 x = 'xxx'; y = 'xxx' 转换成
* var a = 'xxxx'; x = a; y = a; 默认为不转换,为了达到更好的压缩效果,可以设置为false
*/
reduce_vars: true
}
}
})
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 优化产出代码
# 1.设置production模式
设置 production 模式也是优化的方法之一。(虽然这是已经是一个非常常用的配置了)
module.exports = {
mode: 'production'
}
2
3
设置 production 模式有以下好处:
- 自动压缩代码(webpack4.x后支持),也可以自己配置 ParallelUglifyPlugin
- vue、react 等会自动删除调试代码(如开发环境的 warning)
- 自动开启 Tree-Shaking:删除没有使用到模块(必须使用ES6 module(因为是静态引入)才会生效,如果使用commonJS(动态引入无法检测)则不生效)
# 2.压缩JS
Js 压缩可以使用下面的插件实现:
- uglifyjs-webpack-plugin:不支持 ES6 语法,且需 webpackv4.0.0版本以上
- terser-webpack-plugin:可压缩 ES6 代码,webpack v5 自带最新的 terser-webpack-plugin,不需要再次安装
按需选取,如果需要压缩 ES6 代码,就用 terser-webpack-plugin
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
mode: 'production',
optimization: {
minimizer: [
// 使用 terser-webpack-plugin
new TerserPlugin({
terserOptions: {
// ...
}
include: /\/src/, // 明确范围
parallel: true, // 开启多进程
}),
// 使用 uglifyjs-webpack-plugin
new UglifyJSPlugin({
uglifyOptions: {
warnings: false, // 删除无用代码时不输出警告
compress: {
drop_console: true, // 删除所有console语句,可以兼容IE
collapse_vars: true, // 内嵌已定义但只使用一次的变量
reduce_vars: true, // 提取使用多次但没定义的静态值到变量
},
},
output: {
beautify: false, // 最紧凑的输出,不保留空格和制表符
comments: false, // 删除所有注释
},
sourceMap: false, // 是否开启sourceMap
extractComments: false, // 是否提取注释到单独的文件中
cache: true, // 开启缓存
parallel: true // 开启多进程
})
]
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 3.压缩Css
Css 的处理可以使用下面的插件:
- mini-css-extract-plugin:代码分割,将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件
- optimize-css-assets-webpack-plugin:css 压缩
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
optimization: {
minimizer: [
new OptimizeCSSPlugin({
cssProcessorPluginOptions: {
preset: ['default', {
mergeLonghand: false,
}]
}
})
]
},
plugins: [
new MiniCssExtractPlugin({
filename: utils.assetsPath('css/[name].[contenthash:29].css'),
allChunks: true
}),
// 也可以放在这里
// new OptimizeCSSPlugin({
// cssProcessorPluginOptions: {
// preset: ['default', {
// mergeLonghand: false,
// }]
// }
// })
]
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 4.压缩图片
一般图片在使用前,都会使用 tiny.png 手动压缩。如果嫌麻烦也可以借助image-webpack-loader实现自动压缩。
image-webpack-loader
是基于 imagemin 这个 Node 库来实现图片压缩的。
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
}
},
{
loader: 'image-webpack-loader',
options: {
// 压缩 jpeg 的配置
mozjpeg: {
progressive: true,
quality: 65
},
// 使用 imagemin**-optipng 压缩 png,enable: false 为关闭
optipng: {
enabled: false,
},
// 使用 imagemin-pngquant 压缩 png
pngquant: {
quality: '65-90',
speed: 4
},
// 压缩 gif 的配置
gifsicle: {
interlaced: false,
},
// 开启 webp,会把 jpg 和 png 图片压缩为 webp 格式
webp: {
quality: 75
}
}
}
]
}
]
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 5.小图片使用base64编码
关于文件的处理,一般使用下面两个 loader:
其中 url-loader
功能类似于 file-loader
,但是在文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL(base64),可以减少请求次数。
module.exports = {
module: {
rules: [
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000, // 小于100kb的图片转换为base64
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 6.提取公共代码
通过配置 optimization.splitChunks
使用 SplitChunksPlugin 插件可以提取公共模块代码。公共模块文件在最开始的时候加载一次,便存到缓存中供后续使用,大大提升了访问速度。
从 webpack4.0 后,CommonsChunkPlugin 已经被移除,更改为使用 SplitChunksPlugin。
module.exports = {
optimization: {
splitChunks: {
chunks: 'initial', // 代码分隔模式。initial:同步代码分割 ,async:异步代码分割,all:同步异步分割都开启
minSize: 30000, // 字节 引入的文件大于30kb才进行分割
cacheGroups: {
// 创建一个commons块,其中包括入口点之间共享的所有代码
common: {
test: /src/,
chunks: 'initial',
minChunks: 9, // 模块至少使用次数
name: 'common',
priority: 9, // 优先级,先打包到那个组里面,值越大,优先级越高
enforce: true
},
/**
* webpack 默认有 vendor 配置,自定义配置会覆盖原来的
*/
// 创建一个vendors块,其中包括node_modules整个应用程序中的所有代码
vendor: {
test: /node_modules/,
chunks: 'initial',
minChunks: 9,
name: 'vendor',
priority: 10,
enforce: true
}
}
},
runtimeChunk: { // 向仅包含运行时的每个入口点添加一个代码块
name: 'manifest'
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 7.开启Scope Hosting
使用 ModuleConcatenationPlugin 插件可以实现预编译功能。使用这个插件可以将一些工具类模块预编译到一个闭包中,在其他模块加载前先加载这个模块,这种行为称为 Scope Hosting
(作用域提升)。
必须使用 ES6 的 module 语法,否则 webpack 会自动回退到普通打包。
module.exports = {
resolve: {
// 针对 npm 中第三方模块优先采用 jsnext:main 中指向的 ES6 模块语法的文件
maniFields: ['jsnext:main', 'browser', 'main']
}
plugins: [
// 开启 Scope Hosting
new webpack.optimize.ModuleConcatenationPlugin()
]
}
2
3
4
5
6
7
8
9
10
ModuleConcatenationPlugin
可以实现代码合并(必须使用ES6 module 的文件),它有以下好处:
- 代码体积更小
- 创建函数作用域更少
- 代码可读性更好
# 8.开启gzip压缩
使用 compression-webpack-plugin 可以提供带 Content-Encoding
编码的压缩版的资源。如下图:
注意:需要服务端同步支持。
开启gzip压缩
const CompressionWebpackPlugin = require('compression-webpack-plugin');
module.exports = {
mode: 'production',
plugins: [
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp('\\.(js|css)$'),
threshold: 10240,
minRatio: 0.8
})
]
};
2
3
4
5
6
7
8
9
10
11
12
13
14
服务端支持(nginx)
server {
listen 9000;
server_name localhost; # 您的实际 ip 或者域名
client_max_body_size 20M;
root /home/data; # 静态资源文件存放路径
# gzip
gzip on;
gzip_min_length 1k;
gzip_vary on;
gzip_buffers 4 16k;
gzip_comp_level 2;
# 所有静态资源及缓存
location ~* .*\.(jpg|gif|jpeg|css|png|js|wasm) {
expires 10d;
}
# ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
压缩前资源大小和请求时间:2.5MB,30s
压缩后资源大小和请求时间:626KB,1.22s
基本原理
浏览器请求资源文件时会自动带一个
Accept-Encoding
的请求头告诉服务器支持的压缩编码类型服务器配置开启gzip选项:接收客户端资源文件请求,查看请求头Content-encoding支持的压缩编码格式,如果是包含gzip那么在每次响应资源请求之前进行gzip编码压缩后再响应返回资源文件(在响应头会带上Content-encoding: gzip)
浏览器接收到响应后查看请求头是否带有
Content-encoding:gzip
,如果有进行对返回的资源文件进行解压缩然后再进行解析渲染
注意点
- 低版本浏览器兼容性,服务器可以设置一些忽略规则忽略为浏览器
- 媒体文件无需开启:图片、音乐和视频大多数都已压缩过了,HTML,CSS AND JAVARSCRIPT
- CPU负载:压缩文件耗费CPU(服务器需要压缩文件、浏览器解压文件)
# 9.懒加载
懒加载是基于code-splitting的。不切割代码,所有的文件都揉合在一个 js 里面,就无法实现懒加载了。
关于懒加载,不同的框架有不同的实现,下面介绍基于 vue 的懒加载:
# 1. 路由懒加载
设置 webpackChunkName,可以将某个路由下的所有组件都打包在同个异步块 (chunk) 中。
const router = new VueRouter({
routes: [
{ path: '/foo', component: () => import(/* webpackChunkName: "group-foo" */ './Foo.vue') },
{ path: '/bar', component: () => import(/* webpackChunkName: "group-foo" */ './Bar.vue') }
]
})
2
3
4
5
6
# 2. 组件懒加载(异步组件)
vue 提供多种异步组件注册方式,详细介绍请看官网介绍 - 异步组件
局部注册
<template>
<div id="app">
<async-component></async-component>
</div>
</template>
<script>
export default {
name: 'App',
components: {
'async-component': function (resolve) {
//延时演示
setTimeout(function () {
// 这个特殊的 `require` 语法将会告诉 webpack自动将你的构建代码切割成多个包,这些包会通过 Ajax 请求加载
require(['./components/AsyncComponent.vue'], resolve)
}, 5000);
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 3. vuex状态懒加载
使用 store.registerModule 可以注册一个动态模块。
const store = new Vuex.Store()
...
// 一些条件判断
import('./store/login').then(loginModule => {
store.registerModule('login', loginModule)
})
2
3
4
5
6
7
8
# 10.UI框架按需引入
有时候引入了某个UI框架,如 elementUI
、Ant Design
等,但实际上使用的只有几个组件,如果全部引入显得有点多余。这时可以借助 babel-plugin-component,只引入需要的组件,减少项目体积。
- 安装
babel-plugin-component
npm install babel-plugin-component -D
- 修改
.babelrc
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
2
3
4
5
6
7
8
9
10
11
12
- 使用
import Vue from 'vue';
import { Button } from 'element-ui';
import App from './App.vue';
Vue.use(Button)
new Vue({
el: '#app',
render: h => h(App)
});
2
3
4
5
6
7
8
9
10
# 11.利用Tree-Shaking
在项目中使用 ES6 Modules 语法,以保证 Tree-Shaking
起作用。
如果使用 commonJs 的语法 require
一个模块,是无法 Tree-Shaking
的。
# 12.使用 CDN 加速
通过设置 publicPath
为 CDN 路径,可以加快访问速度( 前提是打包构建后资源需要上传到 CDN)。
使用 CDN 加速不一定需要设置 publicPath,可以让运维在 cdn 配置一个链接指向该项目即可。
module.exports = {
output: {
publicPath: 'https://cdn',
}
}
2
3
4
5
打包后资源路径已经加上了cdn前缀
# 13.利用缓存
利用浏览器缓存文件,当代码文件没变化的时候,用户就只需要读取浏览器缓存的文件即可,提高访问速度。
合理使用缓存流程:
- 代码分割(code splitting):一般来说 javascript 文件使用
[chunkhash]
、css 文件使用[contenthash]
、其他资源(例如图片、字体等)使用[hash]
- 提取公共代码
- 抽取 webpack 的 runtime 代码
- 配置 moduleIds 计算方式
不同版本的配置 moduleIds 计算方式
webpack v4.0
之前需要借助 HashedModuleIdsPlugin 插件配置webpack v4.0
开始可以通过配置optimization.moduleIds
, 默认值是false
webpack v5.0
版本已经默认配置了optimization.moduleIds: 'deterministic'
,不需要手动设置
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].[chunkhash].js', // js 使用 [chunkhash]
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 20000,
// 图片、字体资源使用 [hash]
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
]
},
optimization: {
moduleIds: 'deterministic', // 告知 webpack 当选择模块 id 时需要使用哪种算法
runtimeChunk: 'single', // 将 runtime 代码拆分为一个单独的 chunk
splitChunks: {
cacheGroups: {
vendor: { // 将第三方库提取到单独的 chunk
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
}
}
}
},
plugins: [
new MiniCssExtractPlugin({
// css 文件使用 [contenthash]
filename: utils.assetsPath('css/[name].[contenthash:29].css'),
allChunks: true
})
]
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 其他优化
# 1.升级webpack版本
升级 webpack
的版本,无论是打包构建还是产出代码,都会有大幅的提升。当然,升级后需要做好全面测试。
# 2.区分打包环境配置
开发环境和生产环境的配置应该区分,根据需求进行配置。像HMR
用在生产环境是没有意义的,开发环境使用TerserPlugin
压缩也没必要。
# 3.良好的编码规范
编写代码的时候遵守代码规范,养成良好的习惯。合理规划项目结构,抽离公共组件、公共方法,避免产生冗余代码。使用 ES6 module 好让 Tree-Shaking 生效等等。从每一个小地方做好优化。
# 分析工具
# 体积大小分析
webpack-bundle-analyzer :打包分析神器,通过界面化显示每个包的大小和依赖。
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
2
3
4
5
6
7
运行后会自动打开浏览器,也可以手动打开链接 http://127.0.0.1:8888
# 构建速度分析
使用 speed-measure-webpack-plugin 可以看到构建期间各个阶段花费的时间
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const webpackConfig = smp.wrap({
module: {
rules: [
// ...
]
},
plugins: [
// ...
]
});
module.exports webpackConfig;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
执行后输出,可以看到每个loader 和 plugin 的执行时间
# 最后
关于优化,其实就是涉及到 压缩
、缓存
、明确打包范围
、抽取公共代码
、按需加载
、预编译
、开启多线程
等方面。
其实上文很多优化方法已经是默认的配置,特别是使用了 vue-cli3 搭建的框架,默认配置还被隐藏了。但还是可以了解下,知道是这些配置做了优化。