老工程打包提速折腾记(上)- roadhog 打包提速

1,862 阅读8分钟

背景

2019年的时候,项目这套前端框架,build常常达到900s以上,开发环境修改代码重新编译也需要10-20s左右,严重影响开发效率,优化刻不容缓。

环境

硬件:I5-6200U 4核 @2.3GHz, 8G内存,固态硬盘。

软件:win10,node v8.12.0, webpack v3.12.0,npm v6.2.0。

前端框架:dva + antd, dva v1.2.1,antd v2.13.13,roadhog v1.3.4。

现状

涉及模块108个,首次构建:87.1s,再次构建:43.3s,打包:760.8s,打包文件大小:90.7M。

改进过程

升级相关构建工具,比如node、webpack,或者roadhog和dva都会多少提升一部分速度,但是盲目升级后续导致的依赖错误排查,甚至相关组件功能不可用是非常令人头疼的,因此当系统已经稳定,并且各业务模块比较繁多的情况下,我们并没有考虑这种方案。

去掉多余模块和无效引用

当前项目是另一个老旧项目改造而来,功能模块上一开始有80%的相似,因此项目初期直接把老项目的前端框架拿来用了,因此存在着很多老项目的模块在当前项目下实际上是无用的;另外代码中充斥着大量无效引用,如下

import xx from 'xxx';

事实上业务实现并没有用到XX,必须及时删除已经停止使用的相关库,从源码中移除相关依赖。

通过去掉多余模块和无效引用,打包速度快了100多秒,上面的现状其实已经是经过该部分优化工作后的结果。

但是随着后续业务拓展和迭代,模块还是会越来越多,这种方式的优化还是比较局限。

webpack-visualizer-plugin分析

非常好用的插件,该插件可以让你清楚的看到代码的组成部分,细化到每个文件中,每个模块的百分比,以及在项目中可能存在的多版本引用的问题。

该插件roadhog已内置,只要在webpack中加入以下代码,构建的时候会生成一个stats.html文件。

const Visualizer = require('webpack-visualizer-plugin');
  
webpackConfig.plugins.push(
    new Visualizer()
);

stats.html内容如下:

TIM截图20190201154748.png

发现排第一的echarts占用了2M多,排第二的tf-ag-grid((公司内部使用的封装的一个表格组件))也占用了1.4M,其他的就是css-loader,lodash,moment,antd。

externals

简单来说 external 就是把我们的依赖申明为一个外部依赖,外部依赖通过

针对打包大小占第一的echarts,我们只是在首页用到了,完全可以从项目中剥离出来,采用外部引入的方式。

在 webpack.config.js 中,加入如下代码:

webpackConfig.externals = {
    'echarts': 'echarts'
};

这样我们代码中import echarts from 'echarts'这种情况,echarts实际上是从window全局变量上拿,构建的时候也不会打包进去。当然对应的entry文件就要通过script方式引入。

打包:711.6s,打包文件大小:90M。变化不大。

DllPlugin和DllReferencePlugin

针对lodash和moment这种公共库,同样也可以用external的方式处理,但是我们项目中试了下,在stats.html中仍然赫然在列。经过分析以moment为例,是因为antd中又依赖了moment,导致构建的时候仍然包含进去了。这个时候就可以用DllPlugin的方式来处理。

用过 Windows 系统的人应该会经常看到以 .dll 为后缀的文件,这些文件称为动态链接库,在一个动态链接库中可以包含给其他模块调用的函数和数据。我们把这种思路用到web构建中,为什么会大大提升构建速度呢? 原因在于包含大量复用模块的动态链接库只需要编译一次,在之后的构建过程中被动态链接库包含的模块将不会在重新编译,而是直接使用动态链接库中的代码。 由于动态链接库中大多数包含的是常用的第三方模块,例如 react、react-dom,只要不升级这些模块的版本,动态链接库就不用重新编译。

Webpack 已经内置了对动态链接库的支持,需要通过2个内置的插件接入,它们分别是:

  1. DllPlugin 插件:用于打包出一个个单独的动态链接库文件。
  2. DllReferencePlugin 插件:用于在主要配置文件中去引入 DllPlugin 插件打包好的动态链接库文件。

这块内容的配置可以参考这里,配置也不算繁琐。简单来说 DllPlugin 的作用是预先编译一些模块,而 DllReferencePlugin 则是把这些预先编译好的模块引用起来。这边需要注意的是 DllPlugin 必须要在 DllReferencePlugin 执行前,执行过一次。

所幸的是低版本的roadhog已经支持了DllPlugin的模式,不过仅仅是对开发环境的支持,正式环境的打包仍然不支持。这边的配置主要参考了这个issue

  1. 默认关闭
  2. 通过 .roadhogrc 的 dllPlugin 配置开启,可以配 exclude 和 include 的包
  3. 提供 roadhog buildDll 命令手动打 dll bundle
  4. 开启 dllPlugin 后,roadhog server 时检到没有 manifest.json,提醒执行 roadhog buildDll
  5. 然后用户还需手动在调试的 html 里引入 roadhog.dll.js

我试了一下,第2条里的exclude和include设置了并没有生效,要开启DllPlugin,只需在.roadhogrc.js里面增加dllPlugin: true即可;同时还得先运行roadhog buildDll脚本预先生成 roadhog.dll.js。

企业微信截图_20210531104514.png

因此在package.json里script下添加如下命令:

"scripts": {
    "dll": "roadhog buildDll"
}

执行npm run dll,生成roadhog.dll.js。

企业微信截图_20210531104632.png

我们可以在node_modules\roadhog-dlls中看到roadhog.dll.js和roadhog.json,roadhog.json描述了动态链接库文件中包含哪些模块,其中就包含了lodash和moment。

并在entry文件index.ejs中引入,初次构建 62.3s,再次构建 27.5s。用webpack-visualizer-plugin分析了下,lodash和moment已经不参与构建了,但是antd仍然会参与构建,这点roadhog的作者也说了。dll的方式并不会影响生产打包,因此这种方式我们只针对开发环境,速度提升也是非常明显。对于生产打包,我们对几个打包大头文件采用commonChunkPlugin的方式。

commonChunkPlugin

看一下目前的打包文件大小,打包后js文件110个,将近 90M,有49个js文件达到1M以上。

TIM截图20190202090625.png

我们通过stats.html分析看到,体积过大主要是重复打包了tf-ag-grid、moment、antd,因此我们的思路是通过CommonsChunkPlugin 将公用模块抽离到 vendor.js。我们在webpack.config.js中针对线上环境加入以下代码(开发环境已经用DllPlugin优化):

if (env === 'production') {
    webpackConfig.entry = {
        index: './src/index.js',
        vendor: [
            'moment',
            'lodash',
            'react',
            'react-dom',
            'tf-ag-grid'
        ]
    }
    webpackConfig.plugins.push(
        // 抽取出通用的部分
        new webpack.optimize.CommonsChunkPlugin({
            name: ['vendor'],
            filename: '[name].[hash].js',
            minChunks: Infinity,
        })
    )
}

打包测试结果如下:

tf-ag-grid不参与公共提取打包:打包时间 656.4s,打包文件大小 66.6M,大部分文件体积瘦身 300-400KB

TIM截图20190202094119.png

tf-ag-grid参与公共提取打包:打包时间 355.0s,打包文件大小 34.0M,部分文件体积瘦身达1M以上。

TIM截图20190202095651.png

可以看到tf-ag-grid参与公共打包后,打包效率有了巨大的飞跃,结果令人满意。当然如果把项目中antd里面使用频率较高的模块如果也单独抽离打包到antd.js中的话,效果肯定会更好,这里就不做展开,留待后续优化,可以参考这个issue 解决 roadhog 单页应用提取公共模块的问题

测试结果

优化动作初次构建时间(s)再次构建时间(s)打包时间(s)打包文件大小(M)
未优化87.143.3760.890.7
externals85.339.6711.690
DllPlugin62.327.5720.190
commonChunkPlugin(exclude tf-ag-grid)60.528.7656.466.6
commonChunkPlugin(include tf-ag-grid)62.829.6355.034.0

最终方案

  1. echarts配置externals,通过script引入地址;
  2. 开发环境配置DllPlugin,预编译常用模块;
  3. 线上环境通过commonChunkPlugin抽离公共模块,提升打包速度。

后续研究

  1. 此次打包速度研究,还尝试过happyPack、PrefetchPlugin、webpack-uglify-parallel,但是要么受限于roadhog,要么优化速度不明显,不知道是配置的问题还是其他,作为后续研究;
  2. 对于antd,后续可以用commonChunkPlugin抽离高频模块,继续提高打包速度。

最后

其实还有一个拖慢打包速度的元凶,这个幕后黑手不仅拖慢打包速度,还影响svn更新、npm install甚至文件复制速度,就是IT给我们安装的McAfee。当然这个我们也没有办法,毕竟公司安全需要。最终我们还是要就事论事,从我们的框架层面去优化。

参考