记一次webpack构建提速

编者按:本文作者辛昌浩,360奇舞团前端工程师。

写在开头

由于业务调整需要,最近接手了公司内部云平台的项目H。看了代码,开发了几个需求,我的第一感觉是,H项目真的是严肃又有历史感!因为它经过了好多位前端同学多年的开发与维护,“前任”小伙伴们在里面花费了大量的时间与精力。这里面涉及到的技术栈也错综复杂,包含了react、webpack、reflux、mobx以及不少手动封装的类库和组件,日积月累,已经包含了十几个子项目,代码体积可见一斑。

发现问题

随着时间的迁移和代码体积越来越庞大,开发体验的问题也就随之出现。比如,使用webpack-dev-server启动服务时,你可能需要等待90s;又比如,改一行代码调试bug时,你可能需要等待35s。这是在8g运行内存的MacBook上面的开发体验,如果你的电脑运行内存比这个低的话,可能会更久……

额…,启动了一下项目,106秒…(综合一下更高的电脑配置,这里统一按90s计算)

想象一下,改一个小bug,启动devServer,等90s,加一行代码保存,再等35s,果断不能忍:no_good:‍♂️。

怀着内心的崇敬,以及小心谨慎的态度,我决定对它优化一波。

解决思路

一方面,H项目的webpack配置是一个典型的多入口类型,每次打包出来的代码包含了十几个子项目模块。但是一般的开发需求往往集中在一个子项目中去开发,所以只需要打包某个具体的子项目就满足了。

另一方面,项目构建打包的大部分时间花费在了loader上面,其中主要是babel-loader和eslint-loader,如果把loader编译的结果缓存下来应该能有效缩短构建时间。

OK,有了思路,下面便开始对症下药。

具体实现

第一次优化

出于保密考虑,下文中插图将统一使用demo。

首先先来看一下代码的目录结构。

这里的projectA、projectB、projectC相当于项目H中的各个子项目,他们之间相互没有直接的业务关系,但是共用了一些封装的组件、第三方依赖、公共样式和其他配置。一般来说,我们开发业务需求的时候,往往集中在一个项目中,所以并没有必要打包所有项目。

那我们改进的思路是把项目的多个打包入口搞成动态的即可,动态打包的最终理想效果是, npm start + 项目名 ,webpack知道打包某个项目或所有项目。

第一步,把webpack打包的入口配置拎出来,这里为 entry.config.js

此外, devServer 需要知道,具体打包哪个项目?所以,这里用一个 entry.js 文件来保存环境变量,即打包的项目名,就一行代码,

exports.entryName = 'projectA'

第三步,执行脚本(dev用于本地构建, build用于生产环境打包)来修改 entry.js ,这里以 devServer 本地构建为例

let projectName = process.argv[2] || "all";

let fs  = require ( “fs” ) ;

fs . writeFileSync ( “./config/entry.js” , `exports.entryName = ‘ ${ projectName } ‘` ) ;

let exec  = require ( “child_process” ) . execSync ;

exec ( “npm run serve” , { stdio : “inherit” } ) ;

dev.js 和 build.js为测试和生产环境的打包脚本,接收打包的项目名并写入entry.js,然后启动 devServer 或者 npm run build

修改package.json文件用来执行这两个脚本,

"scripts": {

“start” : “node config/dev.js” ,

“dist” : “node config/build.js” ,

“serve” : “vue-cli-service serve” ,

“build” : “vue-cli-service build” ,

“lint” : “vue-cli-service lint”

}

最后一步,配置webpack打包入口起点。(做demo使用的是vue-cli,所以只需要简单修改下vue.config.js,webpack配置同理修改即可)

const pagesConfigObj = require("./config/entry.config");

module . exports  = {

pages : pagesConfigObj ,

lintOnSave : false

} ;

然后看下完整的entry.config.js,

const entryObj = require("./entry");

const configObj  = {

//项目A

projectA : {

entry : “src/projects/projectA/main.js” ,

template : “public/projectA.html” ,

filename : “projectA.html”

} ,

//项目B

projectB : {

entry : “src/projects/projectB/main.js” ,

template : “public/projectB.html” ,

filename : “projectB.html”

} ,

//项目C

projectC : {

entry : “src/projects/projectC/main.js” ,

template : “public/projectC.html” ,

filename : “projectC.html”

}

} ;

const obj  = entryObj . entryName  === ‘all’ ? configObj  : { [ ` ${ entryObj . entryName } ` ] : configObj [ entryObj . entryName ] } ;

module . exports  = obj ;

到了这里,第一次的优化就完了。

总结一下,以构建子项目projectA为例。 npm start projectA ,执行dev.js脚本,把projectA传给脚本。 process.argv[2] 拿到项目名称,写入到entry.js文件。已经可以动态获取想要打包的项目名称了,然后通过entry.config.js动态导出打包入口配置,修改webpack配置便达到了目的。

这里是第一次优化的demo

第二次优化

第一次优化完,虽然达到了目的,但是迟迟没有提pr。原因有二,一是写文件的操作和使用 child_process 的方式让我感觉姿势不是很优雅;二是我们可能需要处理 stdout 和 stderr 展示到 terminal 中。困惑之余咨询了一下文蔺,后面又改进了一版。

这里以 npm start 为例,执行了start.sh脚本,并通过cross-env保存环境变量,即子项目名。

然后,从进程中获取动态的子项目名,如果没有子项目名,则默认打包所有子项目。

let entryName = process.env.APP_ENTRIES || "all";

相比于第一次,去掉了不必要的写文件操作和使用子进程,优雅多了。

好了,试一下项目H的构建速度,这里只启动最常用的用户端为例,

这里是第二次优化的demo

第三次优化

40多秒,舒服了一点,暂且认为时间缩短了一半,为什么还这么慢?看了一下耗时,babel-loader和eslint-loader显然十分耀眼。

由于业务需求逐渐增加,代码体积越来越大,每次执行构建的时候,babel-loader和eslint-loader会把所有的文件都重复编译一遍,显然有些吃力。

这样的重复工作是否可以被缓存下来呢?答案肯定是可以的,其实大部分 Loader 都提供了 cache 配置项,比如在 babel-loader 中,可以通过设置 cacheDirectory 来开启缓存,babel-loader 就会将每次的编译结果写进硬盘(默认node_modules/.cache/babel-loader)。

除此之外,还可以使用cache-loader, 这也是我在项目中采用的方案。它所做的事情很简单, babel-loader 开启 cache 后做的事情,将 loader 的编译结果写入硬盘(默认node_modules/.cache ),再次构建如果文件没有发生变化则会直接拉取缓存。

使用很简单,如官方 demo 所示,只需要把它放在代价高昂的 loader 的最前面即可。

module.exports = {

module : {

rules : [

{

test : /\.js$/ ,

use : [ ‘cache-loader’ , ‘babel-loader’ , ‘eslint-loader’ ] ,

include : path . resolve ( ‘src’ ) ,

} ,

] ,

} ,

} ;

OK,再次感受一下项目H的构建速度,以只启动最常用的用户端为例,

17秒,这样的构建速度我认为还能接受,感觉两个字,通畅!

写在结尾

关于此次优化,一是通过环境变量动态构建子项目,二是将loader的编译结果缓存,最终缩短了60%以上的构建时间,大大提升了开发体验。然而,随着业务需求的增加,构建时间肯定还会越来越长。我也在思考,是否可以更加细化的进行构建打包,具体到模块甚至是组件呢?后续有待研究…

需求不息,优化不止,希望能给优化开发体验、提升构建速度的小伙伴们一个参考。

关于奇舞周刊

《奇舞周刊》是360公司专业前端团队「 奇舞团 」运营的前端技术社区。关注公众号后,直接发送链接到后台即可给我们投稿。