NEJ Build 太慢怎么办?试试 MOOC NEJ 吧,只需两步,提升 70% 构建性能!
由于历史包袱,中国大学 MOOC(简称中 M)的主站工程生产构建时间大约在 21 分钟,构建采用 NEJ Build,由于 NEJ 当前已无人维护且在部门内应用较多,因此中 M 通过 fork 原工程在保留 NEJ 原有功能的前提下,将 NEJ 的核心打包流程进行改造升级,并提出一套通用解决方案即 MOOC NEJ。MOOC NEJ 已于今年年初上线验证,构建时长缩短至 6 分钟,提升 70%构建性能,经过 8 个月的线上稳定运行,暂未发现引迁移引起的问题。
一、历史背景
中国大学 MOOC(以下简称中 M)和许多网易前端 er 一样在过去几年搭建工程使用的框架为 RegularJS 与 NEJ,同时在打包上采用 NEJ 的工具集 toolkit2 即 NEJ Build(https://github.com/genify/toolkit2)
在项目的开始总是愉快的,那时候开发维护 NEJ 的人还在(甚至还可以提一些 feature),业务项目的人也还在,代码量总是清清爽爽,构建的时光总是“咻”一下就过去了。
然而!时间过去了三五年,当我接手到中 M 主站这个项目的时候,它的构建时间已经达到了将近 21 分钟。
平时等就等呗,最多在后端大佬部署的时候提前构建好等着,要命的是上完线已经 10 点了,这时候测试大佬来报:后端没问题了,前端这里有个问题修一下。
然后,大概就是修复 5 分钟,打包验证半小时。。。。。同时保佑不要再出(deng)一(ban)个(ge)问(xiao)题(shi)。
二、究竟为啥这么慢?
速度慢的原因有两方面:
1.中 M 工程架构:
由于历史原因,中 M 的主站将 web 和 mobile 端以及一并放在 front-main 项目下,同时,加载了两个看不出来有什么区别的超大 lib,导致需要打包的文件非常多,东西多了自然就蚌埠住了。
2.NEJ 合并策略:
toolkit 有一个合并策略的参数来决定一个文件引用计数超过多少次将合并入 core.js,当这个参数设置的越大,core.js 的 size 将越小,构建时间就越长。我们之前为了快速提高开发效率,在测试环境将该参数调至 6,打包时间约 11 分钟,而预发和线上该参数在 13,打包时间约 21 分钟。(毕竟我们不能因为想要打包快,就粗暴的使生产环境的资源变大😖)
三、NEJ 的合并策略为何影响打包速度【核心原理】
由于 NEJ 已早早无人维护,但万幸它是开源的,咱们虽然找不到作者,但是可以通过对 toolkit2 的源码阅读来找到答案,我大致梳理了一下 NEJ 打包的流程。
1. toolkit2 全过程使用同步打包,每一步的处理结果都通过 one by one 的接力形式来传递。
2. toolkit2 打包的本质是通过对合并策略(就是二.2 提到的参数)等参数将每个文件的代码转成抽象语法树(AST),再对 AST 通过 UgilifyJS 进行压缩混淆。
看起来这个流程没什么问题,因为 AST 和 UgilifyJS 在现在也都是很主流的操作(如 webpack 也是使用 UgilifyJS),对前端来讲是再熟悉不过了。
那么为什么会导致打包慢呢?直到我看到了 AST 的生成过程:
先将所有文件通过 concat 生成一个大 AST(源码的 lib/adapter/script.js 的_mergeCodeAndToAST
2.对且仅对这一个 AST 进行 Uglify 压缩混淆(源码的 lib/adapter/script.js 的 parse)
结合中 M 主站的历史架构,当我打印出这一个 AST 时, 单主站 web 端的 Core.js 就大概由 400 个小 AST 形成。。。。
然后它就开始压缩。。。。
然后它就卡住了。。。。住了。。。。了。。。。
四、解决方案考量
针对当前我们对自身工程和 NEJ 的了解,我们大概有以下几个思路去对打包时间进行优化:
【针对中 M 主站工程】
老生常谈的拆分工程 &重构迁移:拆分需要对业务足够了解,才能对其做领域拆分。重构代价太大。同时功能回归点太多。目前一部分功能由于迭代需要已用 React+Webpack 做了替换。
webpack 替换 nej:需要兼容 regular 和 nej 模块,且对老项目内部也要替换 webpack 所需插件,成本较大。
【针对 NEJ 工具集】
Uglify2.0 升级 3.0:3.0 有一个快速打包模式,但经过测试,3.0 有部分不向下兼容的 api,nej 有用到。(此处有踩坑经验)
多进程打包:不改造打包产物,只变更打包流程,成本可控,上线后风险较小,如遇到紧急问题可快速回滚至 nej build,风险可控
最终,我们选择了给 NEJ 架上 webpack 同款多进程功能,打包需要什么,我们就造什么。
五、实现架构
我们并不想多造一个轮子,来做一些颠覆性的改变,使性能得到提升的同时,写的人也难受(考虑太多写一些冗余代码),使用的人也难受(迁移成本高)。
因此我们选择将 toolkit2 fork 下来,将其所有的 api 保留、功能保留,不影响任何老功能的使用姿势,只修改关键路径,缩小改造和迁移成本。
最后生成的库是:@edu/toolkit3 http://npm.hz.netease.com/package/@edu/toolkit3
我们的大致思路是这样:
1. 不对 ast 做 concat 操作,每个文件单独 uglify。
2. 参考 uglify-webpack-plugin 插件的多进程思路,将每个文件作为 task,并发打包。 (我又去看了一下 uglify-webpack-plugin 的源码,做了一张图,毕竟知己知彼才能顺利改造。)
3. 将同步流程改成异步,完成所有 task 后回调结果,调用后续操作
六、代码实现
fork 目录后,新增一个 cluster 文件夹用于存放多进程流程。
1. minifiy.js:执行 ugilify 压缩操作,生成压缩混淆后的 ast 和 string 代码;
2. TaskRunner.js:利用 worker-farm 来进行任务的分发、执行、计数、回调等;
3. worker.js: worker-farm 必须要新建一个 workerfile 才能使用,这个 workerfile 用于承接;
再配合修改原文件 lib/adapter/script.js
_mergeCodeAndToAST:取消 concat 以及 this.ast 的生成;
parse:增加 callback 参数,根据 filename、file、minify 参数创建 task,run each task,并执行 callback;
配合修改原文件 lib/deploy.js
_afterResPrepared:将最后的 embed(将变量嵌入 css、js、html)操作改成异步,作为 callback 传入,等待完成所有的压缩后,执行最后输出前的 embed 操作。 (详细代码可以前往仓库查看)
七、使用方式 &迁移(安利)
说了这么久,终于要开始迁移,还记得标题吗,
只需两步!无忧迁移!不影响任何老功能的使用!
第一步:安装 mooc-nej(目前稳定版本在 0.3.0,0.4.0 尚属 beta 版本,正在内测中)
npm install @edu/toolkit3 -g
第二步:使用它,甚至不用修改命令,只需将 nej build 替换为 mooc-nej 即可
backup:如果你发现有问题,还可以快速回滚到 nej build。
八、生产验证
中 M 在年初就已经使用上了 mooc-nej,目前稳定运行半年有余,暂未发现任何由迁移引起的 bug。请大家放心食用。
九、总结
大概从开始改造到结束改造用了去年 12 月的日常空余时间,其间的心路历程大致是:
😢(打包好久 555)
🤔(什么东西卡这么久)
😯(哇发现了原因)
😎(改造,冲!)
😏😤😏😤😏😤...(改完了挂了...又改完了又挂了...*n)
🤨(换个思路)
💓(忐忑验证)
😁(成功上线)
但是回头看,这些踩坑都是值得的,我们终于不用再漫长的等待构建了!
-END-
评论