roadhog 构建优化
背景
一个 antd 项目打包时间太长,竟然快二十分钟了,有时还会导致内存溢出,查了一些资料(thanks funfish),解决方法如下
roadhog.js问题
roadhog.js 是类似可配置的 react-create-app,只是这个可配置,也只是部分可配置的,木有办法,只能从源码开始看 webpack 配置。
在进行 npm run build
的时候发现终端有提示 Creating an optimized production build...
的字样,而且出现的时间也挺晚的,以前其他项目上面从未见过,难道是 roadhog 自己的?这个时候 webpack 居然还没有开始构建?抱着疑惑,从 roadhog 的bin/roadhog.js
就开始打印当前时间,再在到开始webpack构建的时候再打印一次时间。
结果这个过程要花上2931ms,还是可以接受的,只是明明第一次的时候记得等了很久的,为什么这次只要3s不到?后面又试了几次,耗时均3s左右,后来想起了Webpack 构建性能优化探索里面提到的初次构建和再次构建的问题,一般再次构建耗时都要比初次构建的要少。会不会第一次比较慢是初次构建,后面都是再次构建呢?初次构建和再次构建有什么区别?百度和谷歌都没有查询到答案,只有该博客提到比较多。为了再现问题,well,重启电脑,再次 npm run build
不就是初次构建吗?结果还正如此。
优化webpack
按照Webpack 构建性能优化探索里面给出的思路,对于webpack的优化,可以从四个维度考量:
- 从环境着手,提升下载依赖速度;
- 从项目自身着手,代码组织是否合理,依赖使用是否合理,反面提升效率;
- 从 webpack 自身优化手段着手,优化配置,提升 webpack 效率;
- 从 webpack 可能存在的不足着手,优化不足,进一步提升效率。
从环境出发这一点,是因为不同的nodejs版本和npm版本,有着显著的性能差异来的。可以这么认为最新版本的nodejs/npm自然有更优秀的性能。由于项目本身用的就是最新版本的环境,所以这里也不加以分析了。
从项目中出发
首先用比较常规的方法,通过 webpack-bundle-analyzer
来查看 webpack 体积过大问题,结果如下图所示:
图挺好看的,乍一看没有什么特别的地方,好像每个打包文件都是由诸多细文件组成的。并从文件大小来看压缩过后都在1M以下,无可厚非。但是细心对比下,还是有不少发现。
案例1:为了实现小功能而引用大型 lib
这里用 webpack-bundle-analyzer
来查看打包过大问题,但是在引用的时候,却发现roadhog原本自身就用了 webpack-visualizer-plugin
插件,只是在analyze指令下才能进入分析,整理之后webpack配置如下:
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
var Visualizer = require('webpack-visualizer-plugin'); plugins: [
new Visualizer({
filename: './statistics.html'
})],
new BundleAnalyzerPlugin()
]
这里 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里面也同样没有用到。通过排除法最后定位到如下:
// @src/index.js
import 'moment/locale/zh-cn'; // @src/router.js
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 本身也提供了许多优化的插件,但是由于经常接触不多,许久后容易遗漏,导致再次学习的成本高。一个好的脚手架,是相当重要的。
webpack 自带的优化
webpack 就有不少内置的插件。
- CommonsChunkPlugin
CommonsChunkPlugin 可以从 module 提取公共 chunk,实现降低模块大小,有利于整体工程打包后的瘦身。
CommonsChunkPlugin 这个插件在Vue-cli中也有用到,如下:
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module, count) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
})
把相同的 chunk 提取出来,命名 vendor 与 manifest,前者是常说的公共 chunk 部分,后者是由于代码变动导致 chunk 的 hash 值变化,导致公共部分在每次打包时都会有不一样的 hash 值,使得客户端无法缓存 vendor。**由于代码变动导致 hash 变化,而生成的代码,自然而然的会落在最后配置的 commonschunk 上面,**所以这部分可以单独提取,命名为 manifest。
在roadhog里面,刚开始看以后没有CommonsChunkPlugin的配置,想着赶紧提个issue,但是后面发现,是通过common.js
引入,只有在roadhog里面配置了multipage选项为true的时候,才执行CommonsChunkPlugin插件。其代码如下:
var name = config.hash ? 'common.[hash]' : 'common';
ret.push(new _webpack2.default.optimize.CommonsChunkPlugin({
name: 'common',
filename: name + '.js'
}));
通过 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里面再提取。
entry: {
//...
antd: [ //build the mostly used components into a independent chunk,avoid of total package over size.
'antd/lib/button',
'antd/lib/icon',
'antd/lib/breadcrumb',
'antd/lib/form',
'antd/lib/menu',
'antd/lib/input',
'antd/lib/input-number',
'antd/lib/dropdown',
'antd/lib/table',
'antd/lib/tabs',
'antd/lib/modal',
'antd/lib/row',
'antd/lib/col'
]
},
//...
new webpack.optimize.CommonsChunkPlugin({
names: ['antd'],
minChunks: Infinity
}),
咦?见证奇迹的时候到了,构建后的项目大小居然小了,整整3M,少了36.64%,厉害了。更惊讶的是,峰值内存减少了180M,减少了24.3%,打包时间减少了26s,直接下降到59196ms,减少25%;这牛逼了。
仔细对比一下,发现原来减少的部分并不是我以为的 antd 组件,antd 组件反而在每个打包文件里面的体积都要更大了,大概多了几kb,而减少的部分却是一些 _rc
开头的组件,这 CommonsChunkPlugin 也是厉害,按需加载部分没有单独打包起来,反而打包了这些组件背后的引用,如 rc-table
。为什么这些组件最后还是没有完整的打包在antd里面呢?难道每次用的都不同?
import { DatePicker } from 'antd';
// / babel-plugin-import 会帮助你加载 JS 和 CSS 转变成下面内容
import DatePicker from 'antd/lib/date-picker'; // 加载 JS
import 'antd/lib/date-picker/style/css'; // 加载 CSS
这没看来,只是典型的引入组件,以及引入css模块而已。这是必然会被打包到公共模块的呀。看了未丑化的代码,发现用同一个组件的话,生成的不同文件 antd 的组件内容是一样的,不存在组件内部不一样导致没有打包在一起的情况。折腾许久后尚未解决,不晓得有没有大神知道。
而且 roadhog.js 的方式不允许添加新的入口,只能直接改源代码。。。这项目要怎么上线呢?难道每次都要自己改一遍?这就是约定和可配置的问题所在了,后面大神的博客也有讨论到,最后的思想还是约定为若干模块,可自选配置,来适合不同的场景。
- DedupePlugin/OccurrenceOrderPlugin
这两个功能在webpack里面很常见,以至于已经被移除了,默认加载包含在 webpack 2 里面了。
CommonsChunkPlugin对项目的优化还是很实在的,能减少不必要的打包,不仅是体积,更多的是从内存和时间上。
webpack外引入的优化
前面提到的 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
按照官网上的介绍: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。
从 webpack 不足出发
- HappyPack
使用 HappyPack,可以利用 node.js 的多进程能力,来提升构建速度。在 webpack 打包的时候,经常可以看到 CPU 和内存飚的非常高,内存可以理解,但是 CPU 为何会如此之高呢?只能说明有大量计算,而 node.js 的单进程在大量计算面前是单薄的。可以在 webpack 中定义 loader 规则来转换文件,让HappyPack来处理这些文件,实现多进程处理转换。
设置如下:
new HappyPack({
threads: 4,
loaders: [{
loader: 'babel-loader',
options: babelOptions
}],
})
{
test: /\.(js|jsx)$/,
include: paths.appSrc,
// loader: 'babel',
loader: 'happypack/loader',
options: babelOptions
}
只是运行结果却不让人满意,打包时间/内存什么都和原先的数据几乎相当。难道和 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 里面的配置:
loaders: [{
loader: 'happypack/loader?id=js',
options: babelOptions
}],
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 是生产环境中必备的,毕竟压缩丑化代码,不仅可以降低客户端加载项目体积,降低打开时间,而且可以防止反向编译工程的可能性。在本文的开头就提到过,首次优化就是针对 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
,设置配置如下:
new UglifyJsPlugin({
cache: true,
uglifyOptions: {
compress: {
warnings: false
},
output: {
comments: false,
ascii_only: true
},
ie8: true,
}
})
初次构建的时候,构建时间较之前多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 就好了,初次构建会慢点,再次构建的话,速度就上天了。
- UglifyJsPlugin 与 CommonsChunkPlugin
最后自然也是要让两者合并试一试,效果如何呢?和为优化之前相比,初次构建,内存减少 120+M,构建时间基本一样,构建项目大小自然还是少了 3M。咋一看好像不怎么样,但是要知道这是用上了UglifyJsPlugin,有缓存的!结果再次构建数据如所想的一样,速度和内存数据,和没有用代码压缩丑化基本一致!
这样 uglifyjs-webpack-plugin 与 CommonsChunkPlugin 在生产环境自然是很好的选择。
本文主要是按照(Webpack 构建性能优化探索)[https://github.com/pigcan/blog/issues/1]介绍到的方法实践
roadhog 构建优化的更多相关文章
- Vuejs项目的Webpack2构建优化
最近在做的项目因为相对较大(打包有100多个chunk),在build构建的时候速度一直上不去,甚是烦恼.由于用的是vue-cli的webpack2模板,一开始并没有想着要对其进行优化,一直觉得是we ...
- NEO4J亿级数据全文索引构建优化
NEO4J亿级数据全文索引构建优化 一.数据量规模(亿级) 二.构建索引的方式 三.构建索引发生的异常 四.全文索引代码优化 1.Java.lang.OutOfMemoryError 2.访问数据库时 ...
- WEB项目构建优化之自动清除CSS中的图片缓存
在web项目构建发布时,经常遇到css中图片的修改优化,那么如何清除图片的缓存成为必须要解决的问题.曾经有过傻傻的方法就是直接在图片后面添加随机数.今天主要是从构建自动化方式来解决这个问题,提高开发及 ...
- 从Mpx资源构建优化看splitChunks代码分割
背景 MPX是滴滴出品的一款增强型小程序跨端框架,其核心是对原生小程序功能的增强.具体的使用不是本文讨论的范畴,想了解更多可以去官网了解更多. 回到正题,使用MPX开发小程序有一段时间了,该框架对不同 ...
- pytorch构建优化器
这是莫凡python学习笔记. 1.构造数据,可以可视化看看数据样子 import torch import torch.utils.data as Data import torch.nn.func ...
- webpack3构建全面提速优化vue-cli
前言 伴随着vue的全球化,各种vue的组件框架越来越完善,从早期的element-ui到vux,iview等越来越多高质量的项目,使用vue进行前端构建已然是一件工程化,模块化,敏捷化的事情 在这其 ...
- Spring+SpringMVC+MyBatis+easyUI整合优化篇(十四)谈谈写博客的原因和项目优化
阶段总结 又到了优化篇的收尾阶段了,这其实是一篇阶段总结性的文章,今天是4月29号,距离第一次发布博客已经两个月零5天,这两个多月的时间,完成了第一个项目ssm-demo的更新,过程中也写了33篇博客 ...
- gradle入门(1-4)多项目构建实战
一.多项目构建 1.多项目构建概念 尽管我们可以仅使用单个组件来创建可工作的应用程序,但有时候更广泛的做法是将应用程序划分为多个更小的模块. 因为这是一个非常普遍的需求,因此每个成熟的构建工具都必须支 ...
- webpack构建react项目(一)
前言 下面是我们使用到技术栈: webpack + react + redux + react-router + react-thunk + ES6 + .... 注意事项: 建议使用npm5.X 或 ...
随机推荐
- ReentrantLock源码
ReentrantLock与Synchronized区别在于后者是JVM实现,前者是JDK实现,属于Java对象,使用的时候必须有明确的加锁(Lock)和解锁(Release)方法,否则可能会造成死锁 ...
- day14
迭代器什么是迭代器(iterator) 器指的是某种工具 迭代指的是更新换代的过程,例如应用程序的版本更新从1.0 变成 1.2 迭代的目的是要根据上一个结果,产生下一个结果,这是一个重复的过程,但不 ...
- Java基础学习-关键字的概述和特点以及常量的概述和分类
1.关键字概述 -被Java语言赋予特定含义的单词 2.关键字的特点 -组成关键字的字母全部小写 -常用的代码编辑器,针对关键字有特殊的颜色标记,非常直观,所以我们不需要死记硬 ...
- 【FJOI 20170305】省选模拟赛
题面被改成了个猪... T1猪猪划船(boat) [题目描述] 6只可爱的猪猪们一起旅游,其中有3只大猪A,B,C,他们的孩子为3只小猪a,b,c.由于猪猪们十分凶残,如果小猪在没有父母监护的情况下, ...
- TCP 传输控制协议
开头先说几个协议: IP:网际协议 TCP:传输控制协议 Http:超文本传输协议 AMQP:高级消息队列协议 一:TCP是什么? TCP(Transmission Control Protocol ...
- DP一下,马上出发
简单DP i.May I ask you for a dance(体舞课软广植入) 这题的状态转移方程为:dp[i][j]=max(dp[i-1][j-1]+a[i][j],dp[i][j-1]);( ...
- gRPC Client Middleware.
中间件想必大家不陌生,今天给大家介绍如何实现中间件以及实现gRPC的客户端中间件. 什么是中间件? https://docs.microsoft.com/zh-cn/aspnet/core/funda ...
- Go-For Range 性能研究
文章转载地址:https://www.flysnow.org/2018/10/20/golang-for-range-slice-map.html 如果我们要遍历某个数组,Map 集合.Slice 切 ...
- C语言--第1次作业
1.本章学习总结 1.1思维导图 1.2本章学习体会及代码量学习体会 1.2.1学习体会 经过一周C语言的正式课堂学习,不同于暑期时扒视频囫囵吞枣式学习,林丽老师的讲解详细异常,尽管已经学习了一部分内 ...
- python-request-各方法使用及格式
Request库方法介绍 方法 说明 requests.request() 构造一个请求,支撑一下各方法的基础方法 requests.get() 获取HTML网页的主要方法,对应于HTTP的GET ...