一个 antd 项目打包时间太长,居然快二十分钟了,有时还会致使内存溢出,查了一些资料(thanks funfish),解决方法以下css
roadhog.js 是相似可配置的 react-create-app,只是这个可配置,也只是部分可配置的,木有办法,只能从源码开始看 webpack 配置。html
在进行 npm run build
的时候发现终端有提示 Creating an optimized production build...
的字样,并且出现的时间也挺晚的,之前其余项目上面从未见过,难道是 roadhog 本身的?这个时候 webpack 竟然尚未开始构建?抱着疑惑,从 roadhog 的bin/roadhog.js
就开始打印当前时间,再在到开始webpack构建的时候再打印一次时间。node
结果这个过程要花上2931ms,仍是能够接受的,只是明明第一次的时候记得等了好久的,为何此次只要3s不到?后面又试了几回,耗时均3s左右,后来想起了Webpack 构建性能优化探索里面提到的初次构建和再次构建的问题,通常再次构建耗时都要比初次构建的要少。会不会第一次比较慢是初次构建,后面都是再次构建呢?初次构建和再次构建有什么区别?百度和谷歌都没有查询到答案,只有该博客提到比较多。为了再现问题,well,重启电脑,再次 npm run build
不就是初次构建吗?结果还正如此。react
按照Webpack 构建性能优化探索里面给出的思路,对于webpack的优化,能够从四个维度考量:webpack
- 从环境着手,提高下载依赖速度;
- 从项目自身着手,代码组织是否合理,依赖使用是否合理,反面提高效率;
- 从 webpack 自身优化手段着手,优化配置,提高 webpack 效率;
- 从 webpack 可能存在的不足着手,优化不足,进一步提高效率。
从环境出发这一点,是由于不一样的nodejs版本和npm版本,有着显著的性能差别来的。能够这么认为最新版本的nodejs/npm天然有更优秀的性能。因为项目自己用的就是最新版本的环境,因此这里也不加以分析了。ios
首先用比较常规的方法,经过 webpack-bundle-analyzer
来查看 webpack 体积过大问题,结果以下图所示:git
图挺好看的,乍一看没有什么特别的地方,好像每一个打包文件都是由诸多细文件组成的。并从文件大小来看压缩事后都在1M如下,无可厚非。可是细心对比下,仍是有很多发现。github
案例1:为了实现小功能而引用大型 libweb
这里用 webpack-bundle-analyzer
来查看打包过大问题,可是在引用的时候,却发现roadhog本来自身就用了 webpack-visualizer-plugin
插件,只是在analyze指令下才能进入分析,整理以后webpack配置以下:npm
1 var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 2 var Visualizer = require('webpack-visualizer-plugin'); 3 4 plugins: [ 5 new Visualizer({ 6 filename: './statistics.html' 7 })], 8 new BundleAnalyzerPlugin() 9 ]
这里 webpack-bundle-analyzer
能够给予直观的总体感觉,而 webpack-visualizer-plugin
则细化到每一个文件中,每一个模块的百分比。
首先看到的是:最大的文件打包压缩后是815.9kb,相对于其余较大的文件大出了整整400kb,这里确定是有什么问题,细看以后,发现用了支付宝的 G2,在代码中的体现是:
import G2 from 'g2'; // 将整个G2都引入进来了,致使文件过大
遗憾的是目前G2没有实现按需加载的功能,在issue里面也只是表示正在讨论而已(庆幸这里只是用了G2,没有用到Data-set)。
仔细看了每一个js文件打包构造后,发现有个文件也用了 moment 模块,在印象中是基本没有用到的。moment 模块大小为53.2kb,而在总的打包文件中占 131.6kb。正同 Webpack 构建性能优化探索所说的,若是不想简单实现,就采用 fecha 库来代替 moment,fecha 要比 moment 小不少。只是替换后,发现 moment 体积并无下降多少,因为出处是在 index.js 文件里面,可能的地方只有 dva 了。只是 dva 怎么可能用到moment?彻底不可能的,他的package.json里面也一样没有用到。经过排除法最后定位到以下:
1 // @src/index.js 2 import 'moment/locale/zh-cn'; 3 4 // @src/router.js 5 import { LocaleProvider } from 'antd';
第一个行代码是直接使用了 moment 模块,该代码看着做用不大,并且查阅Ant-Design-Pro的历史版本,均没有发如今index.js里面使用 moment/locale/zh-cn
。细心观察,发如今 index.js里面使用了 moment/locale/zh-cn
以后,其余几处用到moment的地方,生成文件都没有明显 moment 包,这些文件的体积基本上要减小一个 moment 的大小。这个moment/locale/zh-cn
,还能下降其余文件体积。
第二行代码,是在 router.js 文件里面,因为使用了 LocaleProvider 组件,这个组件经过源码能够发现直接引用了 moment 模块 import * as moment from 'moment'
。固然一样也起到了 moment/locale/zh-cn
的效果,能下降其余本来含 moment 文件的体积。
案例2:废弃依赖没有及时删除
项目中用的是 Ant Design,import 的时候,组件是按需加载的,并不会整个引入 Ant Design,可是因为敏捷开发周期较短,新建页面不会从零开始写,基本都是移植类似的页面,由此致使了Ant Design组件的乱引用。
因为 G2 的不可按需加载,以及 moment 在 Ant-Design 中的做用,工程的打包体积和打包时间没有较大的减小。
webpack 自己也提供了许多优化的插件,可是因为常常接触很少,许久后容易遗漏,致使再次学习的成本高。一个好的脚手架,是至关重要的。
webpack 就有很多内置的插件。
CommonsChunkPlugin 能够从 module 提取公共 chunk,实现下降模块大小,有利于总体工程打包后的瘦身。
CommonsChunkPlugin 这个插件在Vue-cli中也有用到,以下:
1 // split vendor js into its own file 2 new webpack.optimize.CommonsChunkPlugin({ 3 name: 'vendor', 4 minChunks: function (module, count) { 5 // any required modules inside node_modules are extracted to vendor 6 return ( 7 module.resource && 8 /\.js$/.test(module.resource) && 9 module.resource.indexOf( 10 path.join(__dirname, '../node_modules') 11 ) === 0 12 ) 13 } 14 }), 15 new webpack.optimize.CommonsChunkPlugin({ 16 name: 'manifest', 17 chunks: ['vendor'] 18 })
把相同的 chunk 提取出来,命名 vendor 与 manifest,前者是常说的公共 chunk 部分,后者是因为代码变更致使 chunk 的 hash 值变化,致使公共部分在每次打包时都会有不同的 hash 值,使得客户端没法缓存 vendor。**因为代码变更致使 hash 变化,而生成的代码,天然而然的会落在最后配置的 commonschunk 上面,**因此这部分能够单独提取,命名为 manifest。
在roadhog里面,刚开始看之后没有CommonsChunkPlugin的配置,想着赶忙提个issue,可是后面发现,是经过common.js
引入,只有在roadhog里面配置了multipage选项为true的时候,才执行CommonsChunkPlugin插件。其代码以下:
1 var name = config.hash ? 'common.[hash]' : 'common'; 2 ret.push(new _webpack2.default.optimize.CommonsChunkPlugin({ 3 name: 'common', 4 filename: name + '.js' 5 }));
经过 CommonsChunkPlugin 插件,node.js 在打包的时候,峰值内存增长了40M,就是约5.4%的内存,打包时间延长了大约6s,而构建后项目体积基本不变,what?有点震惊。只有负面效果。。。。看构建文件,只提取了一个公共文件,大小1kb,并且内容为一句普通的错误打印。为何人与人之间没有相互的chunk能够提取呢?
经过反复查 roadhog/ant-design/ant-design-pro 的 issue 都没有相似的问题,彷佛用了 babel-plugin-antd
对 antd 进行按需加载,没有办法将其提取到 vendor 里面了。如若不想不想按需加载,直接用 cdn 不就行了。可是如今想的是只要单独的提取antd里面几个涉及CRUD的重要组件:表格,form,日历这几个组件可否实现单独打包到vendor?难道是我打开方式不对吗?大神里面少 7s,我还多了 6s。。。。
在这个issue里面看到了这种写法,顿时以为没错,就是她了。entry 里面设置多入口,CommonsChunkPlugin里面再提取。
1 entry: { 2 //... 3 antd: [ //build the mostly used components into a independent chunk,avoid of total package over size. 4 'antd/lib/button', 5 'antd/lib/icon', 6 'antd/lib/breadcrumb', 7 'antd/lib/form', 8 'antd/lib/menu', 9 'antd/lib/input', 10 'antd/lib/input-number', 11 'antd/lib/dropdown', 12 'antd/lib/table', 13 'antd/lib/tabs', 14 'antd/lib/modal', 15 'antd/lib/row', 16 'antd/lib/col' 17 ] 18 }, 19 //... 20 new webpack.optimize.CommonsChunkPlugin({ 21 names: ['antd'], 22 minChunks: Infinity 23 }),
咦?见证奇迹的时候到了,构建后的项目大小竟然小了,整整3M,少了36.64%,厉害了。更惊讶的是,峰值内存减小了180M,减小了24.3%,打包时间减小了26s,直接降低到59196ms,减小25%;这牛逼了。
仔细对比一下,发现原来减小的部分并非我觉得的 antd 组件,antd 组件反而在每一个打包文件里面的体积都要更大了,大概多了几kb,而减小的部分倒是一些 _rc
开头的组件,这 CommonsChunkPlugin 也是厉害,按需加载部分没有单独打包起来,反而打包了这些组件背后的引用,如 rc-table
。为何这些组件最后仍是没有完整的打包在antd里面呢?难道每次用的都不一样?
1 import { DatePicker } from 'antd'; 2 // / babel-plugin-import 会帮助你加载 JS 和 CSS 转变成下面内容 3 import DatePicker from 'antd/lib/date-picker'; // 加载 JS 4 import 'antd/lib/date-picker/style/css'; // 加载 CSS
这没看来,只是典型的引入组件,以及引入css模块而已。这是必然会被打包到公共模块的呀。看了未丑化的代码,发现用同一个组件的话,生成的不一样文件 antd 的组件内容是同样的,不存在组件内部不同致使没有打包在一块儿的状况。折腾许久后还没有解决,不晓得有没有大神知道。
并且 roadhog.js 的方式不容许添加新的入口,只能直接改源代码。。。这项目要怎么上线呢?难道每次都要本身改一遍?这就是约定和可配置的问题所在了,后面大神的博客也有讨论到,最后的思想仍是约定为若干模块,可自选配置,来适合不一样的场景。
这两个功能在webpack里面很常见,以致于已经被移除了,默认加载包含在 webpack 2 里面了。
CommonsChunkPlugin对项目的优化仍是很实在的,能减小没必要要的打包,不只是体积,更多的是从内存和时间上。
前面提到的 webpack-bundle-analyzer
和 webpack-visualizer-plugin
插件就是从 webapck 外部引入的,能够很直观的看。
externals 的设置在 Vue 项目里面用的比较多,其中主要 externals 的是 axios, Vue, Vonic, Vue-router
这些。自己体积也不大,并且做为单页面应用仍是很须要的。
可是到了 Ant Design Pro 项目,因为 Ant Desgin 项目自己 CSS + JS
就要1.5M,对于首屏的影响是显著的。虽然能够经过浏览器缓存/cdn缓存的方式来天然优化,可是首次体验仍是不行,仍是按照官网上的介绍来吧。
按照官网上的介绍:DLLPlugin 和 DLLReferencePlugin 用某种方法实现了拆分 bundles,同时还大大提高了构建的速度。具体原理则是将特定的第三方 NPM 包模块提早构建再引入就行了。经过在 webpack 外进行配置,DllPlugin 负责配置输出引用文件 manifest.json,而 DLLReferencePlugin 在webpack的正常配置里面用 manifest.json 就行了。能够避免每次都对 npm 包打包,明明它们就不会改动,直接引用不是更好吗。
在 roadhog.js 里面实现就有点那个了,按照 sorrycc 做者的意思,在生产环境使用 DllPlugin 是不合适,打包大量的 npm 包后,会延长首屏时间,与按需加载矛盾。这点就和 CommonsChunkPlugin 是相同,都是提取第三方库,并且 DllPlugin 是一次打包便可,之后重复用引用,而 CommonsChunkPlugin 是每次打包都要重复提取公共部分,那这两个又有什么区别?
通常 DllPlugin 打的包会包含不少 npm 包,致使体积很大,首次加载天然很差,并且若之后更新某个包,会致使客户端从新下载整个 DllPlugin 的生成文件,对于产品迭代是不友善的。反观 CommonsChunkPlugin,通常提取的公共部分体积较小,例如antd主要组件提取,不到500kb,除非大版本升级,不然客户端是不会从新请求 vendor.js 文件的。
基于上面的观点 DllPlugin 通常用于提高本地开发的编译速度,就是启动项目开发的时候可以快点。只是一天可以启动多少次项目呢,基本都是热更新为主吧。。。。。这么看好像意义不大,就是开发人员的自 hight 而已。
发现原来roadhog本身也有 DllPlugin 的配置,只要在 config 里面添加 dllPlugin: true
就能够了,固然也是仅仅限于开发环境,确定不是生产环境。非常方便,这里就不详细介绍了,感兴趣的能够自行看看这个issue。
使用 HappyPack,能够利用 node.js 的多进程能力,来提高构建速度。在 webpack 打包的时候,常常能够看到 CPU 和内存飚的很是高,内存能够理解,可是 CPU 为什么会如此之高呢?只能说明有大量计算,而 node.js 的单进程在大量计算面前是单薄的。能够在 webpack 中定义 loader 规则来转换文件,让HappyPack来处理这些文件,实现多进程处理转换。
设置以下:
1 new HappyPack({ 2 threads: 4, 3 loaders: [{ 4 loader: 'babel-loader', 5 options: babelOptions 6 }], 7 }) 8 { 9 test: /\.(js|jsx)$/, 10 include: paths.appSrc, 11 // loader: 'babel', 12 loader: 'happypack/loader', 13 options: babelOptions 14 }
只是运行结果却不让人满意,打包时间/内存什么都和原先的数据几乎至关。难道和 CommonsChunkPlugin 的时候同样,又是打开方式不正确?因而按照官网说的加个 id 试试,结果立马报错,提示AssertionError: HappyPack: plugin for the loader '1' could not be found! Did you forget to add it to the plugin list?
,看到有 issue 提出将 loader 里面的 options 改成 query 就能够了,只是官方提示 webpack 2+ 须要使用 options 来代替query ,最后试了一下也是报错,报错的根由是 happyloader 没有获取到查询的识别 id。回头看了下源码,query = loaderUtils.getOptions(this) || {}
这句话不就是获取 loader 的 option 配置吗,里面怎么可能有 id 呢?里面就是 babelOptions,不可能有 id 的。接着看 loader-utils 的源码,这个就是简单的获取查询到的 query,没有毛病,难道是 HappyPack 用错了?
折腾很久后,差很少都要放弃了,我定了定神,从新理一遍,看到了 rules 里面的配置:
1 loaders: [{ 2 loader: 'happypack/loader?id=js', 3 options: babelOptions 4 }],
options 选项是 roadhog 原先就有的,而 laoder 原先是 babel
,后面改成了 happypack 的设置。这个时候眼睛一亮 loader 设置里面有个问号 ?
,这个不就是 query 吗?那 options 呢?loader-utils 里面获取的是这个 query 仍是 option?注释掉试一试?完美成功了。。。。原来如此简单。
用了 happypack 以后,不能在 rules 里面的相关 loader 中配置 options,相反只能在 happypack 插件中配置 options!
well, 然而什么都没有变呀,设置了缓存也没有用,速度/内存什么的都和以前一摸同样。这个时候看到了(在 roadhog 中尝试支持happypack)[https://github.com/sorrycc/roadhog/issues/122]里面大神说了社区版本有问题。。。。。。虽然不知道具体的缘由,可是实际效果是对 js 文件用 HappyPack 的配置,是没有起到想象中的多进程计算的优势的,缘由或许出在 babel/HappyPack 身上了,最后仍是落到了单线程计算上。具体就不分析了,有空能够在研究一下。
uglifyPlugin 是生产环境中必备的,毕竟压缩丑化代码,不只能够下降客户端加载项目体积,下降打开时间,并且能够防止反向编译工程的可能性。在本文的开头就提到过,首次优化就是针对 uglifyPlugin 的,并且效果显著。
使用 webpack.optimize.UglifyJsPlugin
的时候,平均下来 webpack 的构建时间要达到 86s 左右。当不进行代码压缩丑化的话,构建时间降低了 68s 左右,而且构建时候,node.js 占用内存峰值降低了 380M 多,能够说不压缩丑化的话,效果是很是好的。可是项目体积却基本是本来的三倍之大,这是难以容忍的。webpack自带的uglifyPlugin,如此笨拙,要如何处理呢?
对 webpack.optimize.UglifyJsPlugin
在里面添加 cache: true
的配置也是没有什么效果,看了下官网介绍的另一个 UglifyJsPlugin 插件,上面写着 webpack =< v3.0.0
已经包含 UglifyjsWebpackPlugin 的 0.4.6 版本了,若是想要安装最新版本才按照下面介绍的来。发现本地安装的 webpack 版本是 3.11.0,天然是内置 0.4.6 版本。1.0.0 版本是会在 webpack 4.0.0 里面安排的。那若是直接用 uglifyjs-webpack-plugin
最新版本呢?
安装 uglifyjs-webpack-plugin 1.2.2
,设置配置以下:
1 new UglifyJsPlugin({ 2 cache: true, 3 uglifyOptions: { 4 compress: { 5 warnings: false 6 }, 7 output: { 8 comments: false, 9 ascii_only: true 10 }, 11 ie8: true, 12 } 13 })
初次构建的时候,构建时间较以前多40s,也就是多了46.5%,有点夸张的多,内存还好,峰值基本和用 0.4.6 版本的同样。可是 再次构建呢?构建时间竟然降低了68s,并且内存峰值和未用代码压缩丑化的时候类似,也就是减小了 380M,实在厉害,牛逼哄哄。
还能够开启并行,也就是多进程工做,设置 parallel: true
,设置以后测试,初次构建时间竟然比普通的再次构建时间要少10s,可是问题也很明显 CPU 在平时的时候峰值基本在 45% 左右,而多进程后,CPU 的峰值竟然很长一段时间都在 100%,内存也是达到了 1300+M,实在恐怖,若是正式服这么用不晓得会不会爆炸呢?hahaha。parallel 除了能够设置为 true 之外,还能设置成进程数,因而试了等于 2 的时候,CPU 运行峰值接近 95%,而内存峰值在 1100+M,也算是相对较好的数据,只是 CPU 仍是接近于爆表。
对于再次构建 parallel 天然是起不到做用的,这里有不得不提另一个插件 webpack-parallel-uglify-plugin
(下载量比另一款 webpack-uglify-parallel
多上一倍,确定使用这个嘛)。试了一下,初次构建基本和 uglifyjs-webpack-plugin 1.2.2
一致,只有构建时间快 7s。
综上所诉,对于服务器 CPU 豪华的能够考虑平行压缩丑化,通常时候用 uglifyjs-webpack-plugin 1.2.2
多进程就不用设置,使能 cache 就行了,初次构建会慢点,再次构建的话,速度就上天了。
最后天然也是要让二者合并试一试,效果如何呢?和为优化以前相比,初次构建,内存减小 120+M,构建时间基本同样,构建项目大小天然仍是少了 3M。咋一看好像不怎么样,可是要知道这是用上了UglifyJsPlugin,有缓存的!结果再次构建数据如所想的同样,速度和内存数据,和没有用代码压缩丑化基本一致!
这样 uglifyjs-webpack-plugin 与 CommonsChunkPlugin 在生产环境天然是很好的选择。
本文主要是按照(Webpack 构建性能优化探索)[https://github.com/pigcan/blog/issues/1]介绍到的方法实践