19

记一次webpack构建提速

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzA5NzkwNDk3MQ%3D%3D&%3Bmid=2650590840&%3Bidx=1&%3Bsn=a85b2098cf885f6e99c88888e5f11f91
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

写在开头

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

发现问题

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

iu2IVrR.png!web

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

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

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

解决思路

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

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

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

具体实现

第一次优化

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

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

IvmANnr.jpg!web

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

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

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

EBb6f2i.jpg!web

此外, 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" } ) ;

mqmYbqy.jpg!web

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保存环境变量,即子项目名。

miMbqeF.jpg!web

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

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

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

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

RRR7JvV.png!web

这里是第二次优化的demo

第三次优化

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

7zyIVrM.png!web

由于业务需求逐渐增加,代码体积越来越大,每次执行构建的时候,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的构建速度,以只启动最常用的用户端为例,

eeUbEbn.png!web

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

写在结尾

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

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

关于奇舞周刊

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

fI773i6.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK