随着业务复杂度的不断的增长,工程模块的体积也会不断增长,构建后的模块一般要以M为单位计算。在构建过程当中,基于nodejs的webpack在单进程的状况下loader表现变得愈来愈慢,在不作任何特殊处理的状况下,构建完后的多项目之间公用基础资源存在重复打包,基础库代码复用率也不高,这都慢慢暴露出webpack的问题。css
原文地址html
针对存在的问题,社区涌出了各类解决方案,包括webpack自身也在不断优化。前端
下面利用相关的方案对实际项目一步一步进行构建优化,提高咱们的编译速度,本次优化相关属性以下:node
机器: Macbook Air 四核 8G内存react
Webpack: v4.10.2webpack
项目:922个模块git
构建优化方案以下:es6
减小编译体积大小github
将大型库外链web
将库预先编译
使用缓存
并行编译
初始构建时间以下:
增量构建 | Development 构建 | Production 构建 | 备注 |
---|---|---|---|
3088ms | 43702ms | 89371ms |
初始构建时候,咱们利用webpack-bundle-analyzer
对编译结果进行分析,结果以下:
能够看到,td-ui(相似于antd的ui组件库)、moment库的locale、BizCharts占了项目的大部分体积,而在没有所有使用这些库的所有内容的状况下,咱们能够对齐进行按需加载。
针对td-ui和BizCharts,咱们对齐添加按需加载babel-plugin-import
,这个包能够在使用ES6模块导入的时候,对其进行分析,解析成引入相应文件夹下面的模块,以下:
首先,咱们先添加babel的配置,在plugins中加入babel-plugin-import
:
{
...
"plugins": [
...
["import", [
{ libraryName: 'td-ui', style: true },
{ libraryName: 'bizcharts', libraryDirectory: 'lib/components' },
]]
]
}
复制代码
能够看到,咱们给bizcharts也添加了按需加载,配置中添加了按需加载的指定文件夹,针对bizcharts,编译先后代码对好比下:
编译前:
编译后:
注意:bizcharts
按需加载须要引入其核心代码bizcharts/lib/core
;
到此为止,td-ui和bizcharts的按需加载已经处理完毕,接下来是针对moment的处理。moment的主要体积来源于locale国际化文件夹,因为项目中有中英文国际化的需求,咱们这里使用webpack.ContextReplacementPugin
对该文件夹的上下文进行匹配,只匹配中文和英文的语言包,plugin配置以下:
new webpack.ContextReplacementPugin(
/moment[\/\\]locale$/, //匹配文件夹
/zh-cn|en-us/ // 中英文语言包
)
复制代码
若是没有国际化的需求,可使用webpack.IgnorePlugin
对整个locale文件夹进行忽略,配置以下:
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
复制代码
减小编译体积大小完成以后获得以下构建对比结果:
增量构建 | Development 构建 | Production 构建 | 备注 |
---|---|---|---|
3088ms | 43702ms | 89371ms | |
2561ms | 27864ms | 67441ms | 减小编译体积大小 |
为了不一些已经编译好的大型库从新编译,咱们须要将这些库放在编译意外的地方,或者预先编译这些库。
webpack也为咱们提供了将模块外链的配置externals
,好比咱们把lodash外链,配置以下
module.exports = {
//...
externals : {
lodash: 'window._'
},
// 或者
externals : {
lodash : {
commonjs: 'lodash',
amd: 'lodash',
root: '_' // 指向全局变量
}
}
};
复制代码
针对库预先编译,webpack也提供了相应的插件,那就是webpack.Dllplugin
,这个插件能够预先编译制定好的库,最后在实际项目中使用webpack.DllReferencePlugin
将预先编译好的库关联到当前的编译结果中,无需从新编译。
Dllplugin配置文件webpack.dll.config.js以下:
dllReference配置文件webpack.dll.reference.config.js以下:
最后使用webpack-merge
将webpack.dll.reference.config.js
合并到到webpack配置中。
注意:预先编译好的库文件须要在html中手动引入而且必须放在webpack的entry引入以前,不然会报错。
其实,将大型库外链和将库预先编译也属于减小编译体积的一种,最后获得编译时间结果以下:
增量构建 | Development 构建 | Production 构建 | 备注 |
---|---|---|---|
3088ms | 43702ms | 89371ms | |
2561ms | 27864ms | 67441ms | 减小编译体积大小 |
2246ms | 22870ms | 50601ms | Dll优化后 |
首先,咱们开启babel-loader自带的缓存功能(默认其实就是打开的)。
另外,开启uglifyjs-webpack-plugin
的缓存功能。
添加缓存插件hard-source-webpack-plugin
(固然也能够添加cache-loader)
const hardSourcePlugin = require('hard-source-webpack-plugin');
moudle.exports = {
// ...
plugins: [
new hardSourcePlugin()
],
// ...
}
复制代码
添加缓存后编译结果以下:
增量构建 | Development 构建 | Production 构建 | 备注 |
---|---|---|---|
3088ms | 43702ms | 89371ms | |
2561ms | 27864ms | 67441ms | 减小编译体积大小 |
2246ms | 22870ms | 50601ms | Dll优化后 |
1918ms | 10056ms | 17298ms | 使用缓存后 |
能够看到,编译效果极好。
因为nodejs为单线程,为了更好利用好电脑多核的特性,咱们能够将编译并行开始,这里咱们使用happypack
,固然也可使用thread-loader
,咱们将babel-loader和样式的loader交给happypack接管。
babel-loader配置以下:
less-loader配置以下:
构建结果以下:
增量构建 | Development 构建 | Production 构建 | 备注 |
---|---|---|---|
3088ms | 43702ms | 89371ms | |
2561ms | 27864ms | 67441ms | 减小编译体积大小 |
2246ms | 22870ms | 50601ms | Dll优化后 |
1918ms | 10056ms | 17298ms | 使用缓存后 |
2252ms | 11846ms | 18727ms | 开启happypack后 |
能够看到,添加happypack以后,编译时间有所增长,针对这个结果,我对webpack版本和项目大小进行了对比测试,以下:
Webpack:v2.7.0
项目:1013个模块
全量production构建:105395ms
添加happypack以后,全量production构建时间下降到58414ms
。
针对webpack版本:
Webpack:v4.23.0
项目:1013个模块
全量development构建 : 12352ms
添加happypack以后,全量development构建下降到11351ms。
获得结论:Webpack v4 以后,happypack已经力不从心,效果并不明显,并且在小型中并不适用。
因此针对并行加载方案要不要加,要具体项目具体分析。
对于webpack编译出来的结果,也有相应的性能优化的措施。方案以下:
减小模块数量及大小
合理缓存
合理拆包
针对减小模块数量及大小,咱们在构建优化的章节中有提到不少,具体点以下:
前面两点咱们就不具体描述,在构建优化章节中有说。
树摇功能,将树上没用的叶子摇下来,寓意将没有必要的代码删除。该功能在webapck V2中已被webpack默认开启,可是使用前提是,模块必须是ES6模块
,由于ES6模块为静态分析,动态引入的特性,可让webpack在构建模块的时候知道,哪些模块内容在引入中被使用,哪些模块没有被使用,而后将没有被引用的的模块在转为为AST后删除。
因为必须使用ES6模块,咱们须要将babel的自动模块转化功能关闭,不然你的es6模块将自动转化为commonjs模块,配置以下:
{
"presets": [
"react",
"stage-2",
[
"env",
{
"modlues": false // 关闭babel的自动转化模块功能,保留ES6模块语法
}
]
]
}
复制代码
Tree-shaking编译时候能够在命令后使用--display-used-exports能够在shell打印出关于代码剔除的提示。
做用域提高,尽量的把打散的模块合并到一个函数中,前提是不能形成代码冗余。所以只有那些被引用了一次的模块才能被合并。
可能很差理解,下面demo对比一下有无Scope-Hoisting的编译结果。
首先定义一个util.js文件
export default 'Hello,Webpack';
复制代码
而后定义入口文件main.js
import str from './util.js'
console.log(str);
复制代码
下面是无Scope-Hoisting结果:
而后是Scope-Hoisting后的结果:
与Tree-Shaking相似,使用Scope-Hoisting的前提也是必须是ES6模块,除此以外,还须要加入webpack内置插件,位于webpack文件夹,webpack/lib/optimize/ModuleConcatenationPlugin
,配置以下:
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
module.exports = {
//...
plugins: [
new ModuleConcatenationPlugin()
]
//...
}
复制代码
另外,为了更好的利用Scope-Hoisting,针对Npm的第三方模块,它们也可能提供了ES6模块,咱们能够指定优先使用它们的ES6模块,而不是使用它们编译后的代码,webpack的配置以下:
module.exports = {
//...
resolve: {
// 优先采用jsnext:main中指定的ES6模块文件
mainFields: ['jsnext:main', 'module', 'browser', 'main']
}
//...
}
复制代码
jsnext:main
为业内你们约定好的存放ES6模块的文件夹,后续为了规范,更改成module
文件夹。
在咱们实际的项目中,为了兼容一些老式的浏览器,咱们须要在项目加入babel-polyfill这个包。因为babel-polyfill太大,致使咱们编译后的包体积增大,下降咱们的加载性能,可是实际上,咱们只须要加入咱们使用到的不兼容的内容的polyfill就能够,这个时候babel-plugin-transform-runtime
就能够帮咱们去除那些咱们没有使用到的polyfill,固然,你须要在babal-preset-env
中配置你须要兼容的浏览器,不然会使用默认兼容浏览器。
添加babel-plugin-transform-runtime的.babelrc配置以下:
{
"presets": [["env", {
"targets": {
"browsers": ["last 2 versions", "safari >= 7", "ie >= 9", "chrome >= 52"] // 配置兼容浏览器版本
},
"modules": false
}], "stage-2"],
"plugins": [
"transform-class-properties",
"transform-runtime", // 添加babel-plugin-transform-runtime
"transform-decorators-legacy"
]
}
复制代码
webpack对应的缓存方案为添加hash,那咱们为何要给静态资源添加hash呢?
而后,webpack对应的hash有两种,hash
和chunkhash
。
细想咱们指望的最理想的hash就是当咱们的编译后的文件,不论是初始化文件,仍是chunk文件或者样式文件,只要文件内容一修改,咱们的hash就应该更改,而后刷新缓存。惋惜,hash和chunkhash的最终效果都没有达到咱们的预期。
另外,还有来自于的 extract-text-webpack-plugin
的 contenthash
,contenthash针对编译后的每一个文件内容生成hash。只是extract-text-webpack-plugin在wbepack4中已经被弃用,并且这个插件只对css文件生效。
为了达到咱们的预期效果,咱们能够为webpack添加webpack-md5-hash
插件,这个插件可让webpack的chunkhash根据文件内容生成hash,相对稳定,这样就能够达到咱们预期的效果了,配置以下:
var WebpackMd5Hash = require('webpack-md5-hash');
module.exports = {
// ...
output: {
//...
chunkFilename: "[chunkhash].[id].chunk.js"
},
plugins: [
new WebpackMd5Hash()
]
};
复制代码
为了减小首屏加载的时候,咱们须要将包拆分红多个包,而后须要的时候在加载,拆包方案有:
针对第一点第三方包,咱们也在第一章节构建优化中有介绍,这里就不详细说了。
首先是import(),这是webpack提供的语法,webpack在解析到这样的语法时,会将指定的目录文件打包成一个chunk,当成异步加载文件输出到编译结果中,语法以下:
import(/* webpackChunkName: chunkName */ './chunkFile.js').then(_module => {
// do something
});
复制代码
import()遵循promise规范,能够在then的回调函数中处理模块。
注意:import()的参数不能彻底是动态的,若是是动态的字符串,须要预先指定前缀文件夹,而后webpack会把整个文件夹编译到结果中,按需加载。
而后是require.ensure(),与import()相似,为webpack提供函数,也是用来生成异步加载模块,只是是使用callback的形式处理模块,语法以下:
// require.ensure(dependencies: String[], callback: function(require), chunkName: String)
require.ensure([], function(require){
const _module = require('chunkFile.js');
}, 'chunkName');
复制代码
webpack4中,将commonChunksPlugin废弃,引入splitChunksPlugin,两个plugin的做用都是用来切割chunk。
webpack 把 chunk 分为两种类型,initial和async。在webpack4的默认状况下,production构建会分析你的 entry、动态加载(import()、require.ensure)模块,找出这些模块之间共用的node_modules下的模块,并将这些模块提取到单独的chunk中,在须要的时候异步加载到页面当中。
默认配置以下:
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async', // 标记为异步加载的chunk
minSize: 30000,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~', // 文件名中chunk的分隔符
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2, // 最小共享的chunk数
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
复制代码
splitChunksPlugin提供了灵活的配置,开发者能够根据本身的需求分割chunk,好比下面官方的例子1代码:
module.exports = {
//...
optimization: {
splitChunks: {
cacheGroups: {
commons: {
name: 'commons',
chunks: 'initial',
minChunks: 2
}
}
}
}
};
复制代码
意思是在全部的初始化模块中抽取公共部分,生成一个chunk,chunk名字为comons。
在如官方例子2代码:
module.exports = {
//...
optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
};
复制代码
意思是从全部模块中抽离来自于node_modules下的全部模块,生成一个chunk。固然这只是一个例子,实际生产环境中并不推荐,由于会使咱们首屏加载的包增大。
针对官方例子2,咱们能够在开发环境中使用,由于在开发环境中,咱们的node_modules下的全部文件是基本不会变更的,咱们将其生产一个chunk以后,每次增量编译,webpack都不会去编译这个来自于node_modules的已经生产好的chunk,这样若是项目很大,来源于node_modules的模块很是多,这个时候能够大大下降咱们的构建时间。
如今大部分前端项目都是基于webpack进行构建的,面对这些项目,或多或少都有一些须要优化的地方,或许作优化不为完成KPI,仅为本身有更好的开发体验,也应该行动起来。