webpack性能优化
1、前言
随着前端的发展,在一个前端项目中,框架和构建工具已经成了编配,而webpack显然已经成了最火热的构架工具之一。React,Vue,angularjs2等诸多知名项目也都选用其作为官方构建工具,极受业内追捧,随者工程开发的复杂程度和代码规模不断地增加,webpack暴露出来的各种性能问题也愈发明显,极大的影响着开发过程中的体验。

本文旨在分析 webpack 的性能问题,并提供不同的解决方案。
2、性能问题源自何处
- 项目体积过大,有时只是一个小改动,但热更新的全量构建导致编译时间出奇的长。
- 多个模块之间共用基础资源存在重复打包,代码复用率不高。
- 一些具有公共特性的代码没有提取成通用组件。
- 一些代码库被打包在项目中,导致项目编译时间太长;而且不利于做缓存。
- 图片等静态资源没有走cdn
- 单页面项目过大,导致首次加载时间太长
在此我们介绍一款 wepback 的可视化资源分析工具:webpack-visualizer,这款工具可以在webpack构建的时候会自动帮你计算出各个模块在你的项目工程中的依赖与分布情况,方便做更精确的资源依赖和引用的分析。
3、解决方案
我们主要针对不同的性能问题提供不同的解决方案。
3.1 合理去除对一些代码库的构建

从上图中我们不难发现大多数的工程项目中,依赖库的体积永远是大头,通常体积可以占据整个工程项目的7-9成,而且在每次开发过程中也会重新读取和编译对应的依赖资源,这其实是很大的的资源开销浪费,而且对编译结果影响微乎其微,毕竟在实际业务开发中,我们很少会去主动修改第三方库中的源码,所以我们需要通过一些方案来抽离代码库。
1. 使用 externals 配置来提取常用库
externals的官方定义是:防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。
例如,从 CDN 引入 react ,而不是把它打包:
index.html
...
<script src="https://cdn.bootcss.com/react/15.6.1/react.js"></script>
...
webpack.config.js
externals: {
react: 'React'
}
这样就剥离了那些不需要改动的依赖模块,换句话,下面展示的代码还可以正常运行:
import react from 'react';
简单来说 external 就是把我们的依赖资源声明为一个外部依赖,然后通过 script 外链脚本引入。这也是我们早期页面开发中资源引入的一种翻版,只是通过配置后可以告知 webapck 遇到此类变量名时就可以不用解析和编译至模块的内部文件中,而改用从外部变量中读取,这样能极大的提升编译速度,同时也能更好的利用CDN来实现缓存。
2. 利用 DllPlugin 和 DllReferencePlugin 预编译资源模块
我们的项目依赖中通常会引用大量的npm包,而这些包在正常的开发过程中并不会进行修改,但是在每一次构建过程中却需要反复的将其解析,如何来规避此类损耗呢?这两个插件就是干这个用的。
简单来说 DllPlugin 的作用是预先编译一些模块,而 DllReferencePlugin 则是把这些预先编译好的模块引用起来。这边需要注意的是 DllPlugin必须要在 DllReferencePlugin 执行前先执行一次, dll 这个概念应该也是借鉴了windows程序开发中的 dll 文件的设计理念。
相较于 externals ,DllPlugin 的主要是:
- 由于
externals的配置项需要对每个依赖库进行逐个定制,所以每次引入新的代码库的时候都需要手动修改外链的引入,并且在CDN上配置该代码库的资源,过程比较繁琐,而通过dllPlugin则能完全通过配置读取,减少维护的成本。 -
DllPlugin会将多个代码库抽离成一个js资源,可以减少一些script标签。
(1) 配置 dllPlugin 对应资源表并编译文件
dll.config.js
const webpack = require('webpack');
const path = require('path');
const vendors = [
'react',
'react-dom',
'react-router'
];
module.exports = {
output: {
path: __dirname + '/dist',
filename: '[name].js',
library: '[name]',
},
entry: {
lib: vendors,
},
plugins: [
new webpack.DllPlugin({
path: path.join(__dirname, 'dll', 'manifest.json'),
name: '[name]',
context: __dirname,
}),
],
};
然后执行命令:
NODE_ENV=development webpack --config webpack.dll.lib.js
结果会生成一个 manifest.json 文件和一个 lib.js 文件。
-
manifest.json记录了webpack中的预编译信息,这样等于提前拿到了依赖库中的chunk信息,在实际开发过程中就无需要进行重复编译。 -
lib.js就是将配置的代码库编译后生成的文件。
(2) dllPlugin的静态资源引入
生成了 manifest.json 文件和 lib.js 文件之后,我们还要在我们的配置文件中配置 manifest.json,让 webpack 能够不自动编译这些代码库,配置如下:
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./dll/manifest.json'),
})
]
注意:如果你有依赖代码库相同的项目,也可以使用同一份 manifest.json 和 lib.js 文件,只需在配置中将manifest.json引入,在 script 标签中引入 lib.js 即可。
3.2 多入口项目合理提取出公共代码
当项目的入口很多,但是入口文件存在一些公共代码,对所有依赖的chunk进行公共部分的提取的必要性就会发挥出来。
- 默认会把所有入口节点的公共代码提取出来, 生成一个common.js
new webpack.optimize.CommonsChunkPlugin('common.js')
- 有选择的提取公共代码
new webpack.optimize.CommonsChunkPlugin('common.js',['entry1','entry2']);
- 指定模块必须被入口chunk 共享的数目
new webpack.optimize.CommonsChunkPlugin({
name: "commons",
minChunks: 3
filename: "commons.js"
})
- 抽取enry中的一些lib抽取到vendors中
entry = {
vendors: ['fetch', 'loadash']
};
new webpack.optimize.CommonsChunkPlugin({
name: "vendors",
minChunks: Infinity
});
3.3 单页面应用合理分割代码、按需加载
现在很多项目都采用单页面开发,特别是一些移动端的网站;但是当网站规模越来越大的时候,首先出现的问题是 Javascript 文件变得巨大,这导致首页渲染的时间让人难以忍受。实际上程序应当只加载当前渲染页所需的 JavaScript,也就是大家说的“代码分拆" — 将所有的代码分拆成多个小包,在用户浏览过程中按需加载。 通过代码分割,我们得到的效果如下:
分割之前的页面
分割之后的效果
可以很清楚的看到,我们将一个大的js文件拆分成了若干个chunk文件。
我们项目的结构如下:
page
├── home
│ ├── home.js
│ ├── home.scss
├── guide
│ ├── guide.js
│ ├── guide.scss
└── more
│ ├── more.js
│ └── more.scss
└── app.js
按需加载之后,我们需要对Route进行改造,我们将component方法替换成getComponent,让路由去动态的加载组件。 app.js是项目入口,配置如下:
const rootRoute = {
indexRoute: {
getComponent(nextState, cb) {
require.ensure([], (require) => {
cb(null, require('./home'))
})
}
},
getComponent(nextState, cb) {
require.ensure([], (require) => {
cb(null, require('./index'))
})
},
path: '/',
childRoutes: [
require('./guide'),
require('./more')
]
}
render((
<Router
history={hashHistory}
routes={rootRoute}
/>
), document.getElementById('app'))
此处有四个属性:
path
将匹配的路由,也就是以前的 path。
getComponent
对应于以前的 component 属性,但是这个方法是异步的,也就是当路由匹配时,才会调用这个方法。
这里面有个 require.ensure 方法
require.ensure(dependencies, callback, chunkName)
这是 webpack 提供的方法,这也是按需加载的核心方法。第一个参数是依赖,第二个是回调函数,第三个就是上面提到的 chunkName,用来指定这个 chunk file 的 name。
如果需要返回多个子组件,则使用 getComponents 方法,将多个组件作为一个对象的属性通过 cb 返回出去即可。这个在官方示例也有,但是我们这里并不需要,而且根组件是不能返回多个子组件的,所以使用 getComponent。
indexRoute
indexRoute用来显示默认路由,不需要进行按需加载。
childRoutes
这里面放置的就是子路由的配置,这里的子路由都应该是按需加载的。
我们还需要在子路由中进行配置。
home.js
module.exports = require('./home');
由于home是默认的路由,所以不需要进行按需加载
guide.js
module.exports = {
path: '/guide',
getComponent(nextState, cb) {
require.ensure([], (require) => {
cb(null, require('./guide'))
})
}
}
more.js
module.exports = {
path: '/more',
getComponent(nextState, cb) {
require.ensure([], (require) => {
cb(null, require('./more'))
})
}
}
项目经过webpack打包之后,会生成包含子路由的chunk文件,并且在路由切换的时候进行按需加载。
3.4 加快代码压缩速度
UglifyJsPlugin 凭借基于node开发,压缩比例高,使用方便等诸多优点已经成为了js压缩工具中的首选,但是我们在webpack的构建中观察发现,当webpack build进度走到80%前后时,会发生很长一段时间的停滞,经测试对比发现这一过程正是 UglifyJsPlugin 在对我们的 output 中的 bunlde 部分进行压缩耗时过长导致,针对这块我们推荐使用webpack-uglify-parallel来提升压缩速度。
webpack-uglify-parallel 的实现原理是采用了多核并行压缩的方式来提升我们的压缩速度。
使用配置也非常简单,只需要将我们原来webpack中自带的 UglifyJsPlugin 配置:
new webpack.optimize.UglifyJsPlugin({
exclude:/\.min\.js$/
mangle:true,
compress: { warnings: false },
output: { comments: false }
})
修改成如下代码即可:
const os = require('os');
const UglifyJsParallelPlugin = require('webpack-uglify-parallel');
new UglifyJsParallelPlugin({
workers: os.cpus().length,
mangle: true,
compressor: {
warnings: false,
drop_console: true,
drop_debugger: true
}
})
3.5 让loader多进程地去处理文件
happypack 的原理是让loader可以多进程去处理文件,原理如图示:

此外,happypack同时还利用缓存来使得rebuild 更快
var HappyPack = require('happypack'),
os = require('os'),
happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
modules: {
loaders: [
{
test: /\.js|jsx$/,
loader: 'HappyPack/loader?id=jsHappy',
exclude: /node_modules/
}
]
}
plugins: [
new HappyPack({
id: 'jsHappy',
cache: true,
threadPool: happyThreadPool,
loaders: [{
path: 'babel',
query: {
cacheDirectory: '.webpack_cache',
presets: [
'es2015',
'react'
]
}
}]
}),
//如果有单独提取css文件的话
new HappyPack({
id: 'lessHappy',
loaders: ['style','css','less']
})
]
4、结尾
性能优化无小事,追求快没有止境,在前端工程日益庞大复杂的今天,针对实际项目,持续改进构建工具的性能,对项目开发效率的提升和工具深度理解都是极其有益的。

