自从npm和webpack被引入前端开发环境中以后,模块化开发变得越来越流行,一个项目里可能包含着上万甚至数十万个大大小小的包,随着项目越来越大,应用的构建时间也越来越长,极大影响了开发的效率,下面结合实际谈一些自己的看法。
影响构建速度的原因
硬件部分的不再赘述,加钱即可解决,这里要讨论的是如何从软件设计层面优化速度,降低成本
文件数量
这里拿目前团队里最复杂的一个node项目来举例
vue文件数量1201
图片数量 1773
js文件数量 25383
样式文件 1826
目录数量 8345
可以看到文件数量相当庞大,特别是js文件,其中有一大部分是没有用到的,他们散落在node_modules文件夹中,作为npm包的一部分而存在,然而实际被webpack处理的js文件只有一部分。
此外,由于node_modules的存在,项目里的目录数量达到了惊人的一千多个。
node_modules是有很大缺陷的模块管理方案,它使得项目文件夹里充斥数万个小文件,错综复杂的目录结构,读写的时候,这一点对于机械硬盘是一个严峻的考验,如此一来也严重影响node的读写性能,因此测试环境经常出现编译长达3,4十分钟的情况,毕竟测试环境都是性能普通的机器,配的是机械硬盘,即使是发到生产环境,也需要20分钟。
node_modules的缺陷
- 每个项目都有一个node_modules,不能全局共享,其他语言比如JAVA的maven包是全局式的
- npm包缺乏约束,下载下来的npm包里会包含文档,测试文件,有的设置有源文件,而实际上需要的只是某一两个文件
优化方向
使用图片托管服务,配合vscode上传插件,减少项目中的图片数量 项目中并不需要保存图片,因为发布上线以后图片也是放在CDN上,因此开发了一个vscode的图片上传插件,调用公司内部一个静态文件托管平台提供的上传接口,并使用tinyPNG的接口进行压缩,最终返回图片的外链,如此既减少了项目大小也减少了使用webpack处理图片的性能消耗
拆分项目 将项目中不同业务属性的部分单独做成一个项目,比如所有的H5活动页可以单独做成单独的活动系统,订单支付做成单独的订单中心
建立npm包使用规范 使用npm包时应该选用更加流行,更加纯净,更加轻量的包,避免node_modules过于臃肿
建立npm私服 将公用组件封装成npm库,做好按需引入,如此可以减少webpack处理文件的消耗
发布方式
一般前端应用发布时要经历如下三个过程
- npm install
- npm run build
- pm2 start
其中npm install和npm run build最占用时间,梳理一下jenkins的工作方式我们可以找出其中需要优化的点。
优化方向
- 由于测试服务器硬盘性能一般,直接在测试服务器上打包往往速度很慢,因此对于比较庞大的项目,单独使用一台打包机来打包,打包完以后将静态文件拷贝至部署机
- 不必要的npm install 如果项目中的package.json并未改变是不需要重新npm install的,因此需要监控package.json的变动,向jenkins传入是否需要npm install的参数
- 缓存node_modules 这一点需要配合脚本来实现 我会在我后面的文章中详解
webpack优化
减少不必要的处理
首先我们可以通过分析工具webpack-bundle-analyzer工具查看哪些包过大,比如element-ui,lodash,moment等。 对于UI库之类或者其他比较庞大但是更新不多的库的我们可以通过在html文件中添加CDN引用的方式来减少不必要的打包,此外还需设置externals。 lodash,moment的问题在于过于臃肿,一般在使用lodash的时候只会使用其中的几个函数,然而lodash内部相互引用很复杂,使用一个函数往往引入了很多其他的函数,所以对于比较简单的功能实现我们可以使用更加纯净的包而不是一个大而全的库,尤其是moment往往只是用来格式化日期而已,它内部却给我们引入了一个语言包进来。 另外loader提供了exclude参数用来精确指定需要处理的文件,一般node_modules需要排查在外 eslint的校验应该前置,在提交代码的阶段处理,编译的时候不需要打开
减少依赖查找时间
webpack提供了resolve参数用来指定在何处查找模块
为resolve.modules指定node_modules的绝对路径的可以避免层层查找
为常用的目录比如api,commponents指定alias,避免过多的相对路径引用,以提高查找效率
loader用include指定搜索目录,一般处理src目录下的文件即可
DLL
webpack中的dll是将一些比较文档的第三方库单独拿出来打包进一个dll.js的文件中,此文件暴露了一个全局的函数,接受的参数为模块ID,模块ID保存在另外一个mainfest.json文件中,此文件指定了每个模块的ID以及包路径。 相比CommonsChunkPlugin,DLL的区别在于只要不更新dll中的包不需要升级,那么一次dll编译之后,就可以一直使用,无需再编译,hash不会变,缓存一直有效,而CommonsChunkPlugin每次编译都会做处理,hash也会变化,因此它更加适用于做公共业务代码的抽离。
cache
webpack cache-loader可以用来缓存loader的结果,主要是babel和eslint比较耗时,可以为这两步加上缓存
多进程
这方面的代表是happypack,webpack编译过程中有大量的文件需要转换处理,而它是单线程模型,如果有异步操作,只能等待,然后再继续执行,happypack的原理就是开启多个子进程,将可以并行的操作分配到各个子进程去处理,得到结果后再返回给主进程。 但是它的实用性存疑。 happypack是多进程模型,多进程的特点是进程间独立性高,但是上下文切换消耗高,进程间通信消耗高,webpack在处理文件时,既是IO密集型操作也是CPU密集型操作,不管是使用多进程还是多线程来编译,反倒是增加了复杂度。
发布方式优化
整个发布流程中,有如下几个耗时的点
- npm install 如果硬盘性能不行,并且包很多的话,install特别耗时
- 拷贝项目 如果编译机器与发布机器不是同一台,并且有node服务依赖node_modules启动,就必须拷贝node_modules过去,处理小而多的文件特别耗时
- 压缩项目 因为node_modules的存在这一步也很耗时
- npm run build
可以看出,node_modules的存在极大的影响效率,可以从如下几个点优化。
- npm install是只有package.json发生变动时才需要执行的,借助gitlab-api可以监控到当前发布的代码中有没有涉及package.json的改动,若有才执行npm install。
- 缓存node_modules,有的发布平台会执行git clean导致node_modules被清空,可以通过linux的bind mount来缓存node_modules,这样删除的时候不会删除真正的缓存文件
- 使用tar打包项目目录而不是zip的压缩命令,压缩太多的小文件反而占用时间,得不偿失
- 异步备份项目,项目发布时需要对当前版本做备份,这一步可以采用异步执行,用另外的服务来备份文件,不影响发布进程