NEJ Build太慢怎麼辦?試試MOOC NEJ吧,只需兩步,提升70%構建性能!

語言: CN / TW / HK

由於歷史包袱,中國大學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的生成過程:

  1. 先將所有文件通過concat生成一個大AST(源碼的lib/adapter/script.js的_mergeCodeAndToAST

  1. 對且僅對這一個AST進行Uglify壓縮混淆(源碼的lib/adapter/script.js的parse)

結合中M主站的歷史架構,當我打印出這一個AST時, 單主站web端的Core.js就大概由400個小AST形成。。。。

然後它就開始壓縮。。。。

然後它就卡住了。。。。住了。。。。了。。。。

四、解決方案考量

針對當前我們對自身工程和NEJ的瞭解,我們大概有以下幾個思路去對打包時間進行優化:

【針對中M主站工程】 1. 老生常談的拆分工程&重構遷移:拆分需要對業務足夠了解,才能對其做領域拆分。重構代價太大。同時功能迴歸點太多。目前一部分功能由於迭代需要已用React+Webpack做了替換。 2. webpack替換nej:需要兼容regular和nej模塊,且對老項目內部也要替換webpack所需插件,成本較大。

【針對NEJ工具集】 1. Uglify2.0升級3.0:3.0有一個快速打包模式,但經過測試,3.0有部分不向下兼容的api,nej有用到。(此處有踩坑經驗) 2. 多進程打包:不改造打包產物,只變更打包流程,成本可控,上線後風險較小,如遇到緊急問題可快速回滾至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的源碼,做了一張圖,畢竟知己知彼才能順利改造。)

  1. 將同步流程改成異步,完成所有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

  1. _mergeCodeAndToAST:取消concat以及this.ast的生成;
  2. parse:增加callback參數,根據filename、file、minify參數創建task,run each task,並執行callback;

配合修改原文件lib/deploy.js

  1. _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即可

``` // 所有的nej build改成mooc-nej build即可 mooc-nej build deploy/mobile/release.js

// 或者構建機安裝直接引用build文件

```

backup:如果你發現有問題,還可以快速回滾到nej build。

八、生產驗證

12.png 中M在年初就已經使用上了mooc-nej,目前穩定運行半年有餘,暫未發現任何由遷移引起的bug。請大家放心食用。

九、總結

大概從開始改造到結束改造用了去年12月的日常空餘時間,其間的心路歷程大致是:

😢(打包好久555) \ 🤔(什麼東西卡這麼久) \ 😯(哇發現了原因) \ 😎(改造,衝!) \ 😏😤😏😤😏😤...(改完了掛了...又改完了又掛了...*n) \ 🤨(換個思路) \ 💓(忐忑驗證) \ 😁(成功上線)

但是回頭看,這些踩坑都是值得的,我們終於不用再漫長的等待構建了!

-END-