唠叨几句
使用webpack也有一段时间了,从webpack1到webpack3,这中间有太多的曲折与纠结。webpack无疑是强大的,强大到没有朋友,webpack的设计初衷也是伟大的,它可以编译我们的代码,借助各种loader,使得我们可以使用各种es6/es7的特性,可以使用sass/less,可以编写react代码等等。
webpack有很多我们耳熟能详的功能,例如webpack-dev-server, Hot Module Replacement, SourceMap, UglifyJsPlugin, 自动生成html等等,一切都是配置,那么这么多的配置项也带来了配置文件杂乱可读性差的问题,而最让人难以忍受的还是webpack打包的性能问题。
近期接触到一个工程,这个工程模块有100多个,在全量build的时候,一定会报内存溢出,打包失败。即便是模块不太多的工程,也会发现打包速度十分缓慢的问题,从此便可以名正言顺的趁着build的过程去个厕所,喝杯水什么的。
基于性能这一痛点,然后就开始了折腾,想着法对构建的性能做一些提升。
折腾之路
版本升级
随着webpack版本的升级,webpack的配置项开始出现了限制,2.0版本以后webpack的配置项已经不允许出现自定义的属性了,核心就围绕在entry, output, loaders, 和plugins这几项当中,这在一定程度上能让webpack配置文件看起来不那么的乱。
webpack官方也注意到了性能问题,所以在2.0版本以后,很多在webpack1中需要配置的一些提升性能的插件也都做了默认开启,例如OccurrenceOrderPlugin(排序输出), DedupePlugin(删除重复数据)等等,loaders也将默认开启缓存。
2.0以后的版本无论是从优化配置,到向es6 module,Promise等标准接轨,再到编译环境和性能的优化,再到API设计的整体规范性上,相对1.0版本的改进还是非常显著的。
3.0版本的webpack加入了作用域提升的功能,老版本webpack需要将每个模块包裹在单独的函数闭包中实现模块系统。而这些封装函数往往会使得浏览器中运行的javaScript代码性能有所下降。而 webpack3中提供了插件来允许开发者启用作用域提升特性来避免这种额外的性能损耗:
... plugins: [ new webpack.optimize.ModuleConcatenationPlugin() ] ...
作用域提升特性能够移除模块的封装函数,所以引入该插件后打包后代码的体积也会有所减少。所以升级webpack是提升性能的关键。
减少依赖
webpack是把所有的静态资源打包到一个bundle中,通过各种loader对代码进行编译,那么如果依赖的模块过多,这肯定会导致性能下降。所以我们应该尽量减少依赖以及删除那些没有使用到的依赖。
模块 和 依赖位置
通过设置webpack的resolve属性,来指定所加载文件的扩展名以及配置模块库(node_modules)所在的位置,通过配置alias指定路径别名,在代码中尽量使用alias的路径可能提升文件查找速度。webpack配置loader的过程中也可以指定文件路径和排除文件路径。这在一定程度上能让文件查找快起来。
... resolve: { extensions: ['.js'], alias: { globalLib: path.join(__dirname, './src/lib') }, modules: [path.resolve(__dirname, 'node_modules')], }, ... module: { rules: [ ... { test: /\.js?$/, exclude: /node_modules/, include: path.resolve(__dirname, 'src'), use: [ { loader: 'happypack/loader', options: { id: 'js', }, }, ], }, ... ] }, ...
开发环境和线上环境分开配置
开发环境和线上环境的代码要求是不一致的,开发环境更为严苛,可是在开发环境完全不需要这么做,有些插件是十分消耗性能的,那么在开发环境中我们不需要引用,例如optimize-css-assets-webpack-plugin, UglifyJsPlugin, assets-webpack-plugin, DllReferencePlugin等等,这些代码优化压缩层面的插件在线上环境更加重要,那么开发环境完全可以不去加载。我们可以通过定义环境变量NODE_ENV=production的方式,在配置文件中做判断。
... const NODE_ENV = process.env.NODE_ENV; if(NODE_ENV === 'production'){ config.plugins.push( new webpack.optimize.UglifyJsPlugin({ compress: { unused: true, dead_code: true, warnings: false, }, sourceMap: false, parallel: { cache: true, workers: os.cpus().length, }, minimize: true, mangle: { except: ['$', 'exports', 'require'], }, output: { ascii_only: true, }, }), ) } ...
打包DLL
通过使用 DllPlugin & DllReferencePlugin 组件,我们可以对一些公共的库进行独立打包,单独引用,这样在其它代码进行构建的过程中就不需要再去重新编译了。DllPlugin产生vendor.js/vendor.css和manifest.json,vendor.js/vendor.css中保存着打包好的库文件,vendor.js中暴漏到全局一个类似require功能的函数,而manifest.json存储着依赖的资源路径以及id,提供给DllReferencePlugin使用,DllReferencePlugin在处理文件的时候,遇到manifest.json中的资源,使用vendor.js暴漏给全局的方法进行处理,而不会把这个文件打包进来。
// webpack.dll.js ... const vendors = [ 'react', ]; ... module.exports = { output: { path: path.resolve('build'), filename: '[name].js', library: '[name]', }, entry: { vendor: vendors, }, ... plugins: [ ... new webpack.DllPlugin({ path: 'manifest.json', name: '[name]', context: __dirname, }) ... ], ... }; // webpack.config.js ... plugins: [ ... new webpack.DllReferencePlugin({ context: __dirname, manifest: require('./manifest.json'), name: 'vendor', }) ... ], ...
-
happypack可以配置编译缓存,以及充分利用多进程对静态资源进行编译,这在极大程度上提高了代码的构建速度。这里推荐一篇文章happypack 原理解析
// @file webpack.config.js exports.plugins = [ new HappyPack({ id: 'jsx', threads: 4, loaders: [ 'babel-loader' ] }), new HappyPack({ id: 'coffeescripts', threads: 2, loaders: [ 'coffee-loader' ] }) ]; exports.module.loaders = [ { test: /\.js$/, loaders: [ 'happypack/loader?id=jsx' ] }, { test: /\.coffee$/, loaders: [ 'happypack/loader?id=coffeescripts' ] }, ]
利用缓存
充分的利用各种可缓存配置,上述的happypack可缓存,UglifyJsPlugin可缓存,每个loader可以配置缓存等等。
... const os = require('os'); const HappyPack = require('happypack'); ... const createHappyPlugin = function (id, loaders) { return new HappyPack({ id, loaders, threadPool: HappyPack.ThreadPool({ size: os.cpus().length }), cache: true, verbose: true, debug: true, }); }; ... plugins: [ ... createHappyPlugin('js', ['babel-loader?cacheDirectory']) ... new webpack.optimize.UglifyJsPlugin({ compress: { unused: true, dead_code: true, warnings: false, }, sourceMap: false, parallel: { cache: true, workers: os.cpus().length, }, minimize: true, mangle: { except: ['$', 'exports', 'require'], }, output: { ascii_only: true, }, }) ]
提取公共代码
使用CommonsChunkPlugin这个插件,可以把资源中引用的公共代码提取出来,然后我们就可以在页面中进行单独引用。使得工具代码和业务代码可以分离。加之浏览器的缓存文件的能力,也可以提高线上页面加载的速度。
... plugins: [ ... // 把所有入口文件的公共代码提取出来,生成common.js new webpack.optimize.CommonsChunkPlugin('common.js'), ... ], ...
代码分割
通过使用require.ensure()对代码进行按需加载,webpack会收集ensure中的依赖,打包到一个单独的文件中,在用到的时候通过jsonp的形式异步加载进来,通过这种方式进行代码分割以及按需加载,对页面加载性能提升有不小的帮助,但是这个方法需要修改代码,不太适用于老工程改造。
CSS分割
使用extract-text-webpack-plugin对css文件进行抽离,单独引用,这样做可以减小bundle的size,另外css文件是需要加载到dom元素上面,即header中的,无论怎么说,这样做都是明智的。
-
这是一个高性能的sass loader,在比较大的sass工程中,速度是sass-loader的5-10倍。
hash值
通过配置hash值,使用每个文件内容计算出的md5作为文件的版本号,可以极大程度的使用浏览器缓存,提升页面性能,通过assets-webpack-plugin这个插件,可以将hash值生成json文件,方便我们配置。后面介绍这几个,其实是从页面加载性能来考虑问题的,这里也一并提出来了。
知己知彼
webpack构建分析
如果我们对webpack构建的速度不是很满意,我们可以借助webpack-analyse或者webpack-visualizer工具对构建过程进行分析,以便于进一步的优化,做到知己知彼百战不殆,开始之前我们需要使用如下命令先生成一个构建过程的json文件:
webpack --json --profile > stats.json
接下来的过程是可视化的,我们把json文件上传,紧接着就可以看到构建结果了。
随便聊聊
通过以上的优化,对webpack构建的速度有不小的提升。完成这个项目后,对改造前后打包时间做了对比,速度提升了十几倍。但是我想这并不是终极方案,结果也并不是我满意的。相信后续的webpack版本,能够让我们拥有一个配置简单,构建高效,功能强大的工具。文章中有错误的地方,欢迎指出,互相学习,一起进步!Best Regards !