Angular 應用瘦身記——比 jQuery 更小的 TodoMVC
本文內容提取自 《2017成都WEB前端交流大會》 中的主題演講。
眾所周知,Angular 以官方提供的一體化解決方案(全家桶)而聞名,官方團隊提供了構建 Web App 所需的大部分類庫和工具支持。
不過,「大而全」的「大」指的是覆蓋範圍廣,而並非應用體積大,這裡我們以 TodoMVC 為例,將其優化到比 jQuery 更小的體積[1]。
1. 本文寫作時 jQuery 的最新版本為 3.2.1,非 slim 版本 min+gzip 後大小為 33,861B。為 Chrome 中的實際傳輸大小,可能與本地壓縮結果略有差異。
AOT 編譯
Angular 模版採用了編譯到 JavaScript 結構化數據的方式[2],雖然組件模版使用 .html
文件[3]定義,但瀏覽器並不能見到這個 HTML 文件,而只能見到編譯後的 JavaScript 文件。
例如,一個簡單的表單控制項:
<div class="form-group"> <label for="exampleInput">Email address</label> <input type="email" class="form-control" id="exampleInput" [value]="email" (change)="onEmailChange($event)"> <small id="emailHelp" class="form-text text-muted"> We"ll never share your email with anyone else. </small></div>
將會被編譯為[4]:
export function View_AppComponent() { return viewDef( ViewFlags.None, [ elementDef("div", [["class", "form-group"]]), elementDef("label", [["for", "exampleInput"]]), textDef(["Email address"]), elementDef("input", [["class", "form-control"], ["id", "exampleInput"], ["type", "email"]], [[BindingFlags.TypeProperty, "value"]], [[null, "change"]], (viewData, eventName, $event) => { if ((eventName === "change")) { viewData.component.onEmailChange($event) } } ), elementDef("small", [["class", "form-text text-muted"], ["id", "emailHelp"]]), textDef([`We"ll never share your email with anyone else.`]), ], (check, viewData) => { const currVal = viewData.component.email check(viewData, currVal) } )}
對於 Angular 而言,如果這個編譯過程發生在應用啟動之前,就叫做 AOT 編譯;反之若是在應用啟動之後,則為 JIT 編譯。
由於 AOT 編譯過程,我們的發布內容僅有 JavaScript,而不包含任何 HTML,有利於進一步的優化。
2. 僅適用於當前的 4.x 和 5.x 版本,2.x 版本中模版編譯為完整的視圖操作語句,6.x 版本中模版將編譯為邏輯化的模版函數(甚至可以手寫)。
3. 也可能位於內聯在邏輯代碼中的字元串里。
4. 為了可讀性略有簡化。
Closure Compiler
Closure Compiler 是 Google 推出的一款 JavaScript 優化編譯器,用於優化應用體積和執行性能。被視為 Angular 的下一代構建工具[5]之一。
在 ADVANCED
模式下,Closure Compiler 會進行最大程度的內聯,例如以下代碼:
const items = [ { val: 1 }, { val: 2 }, { val: 3 }, { val: 4 }, { val: 5 },]const item = items[2]console.log(item.val)
會被優化為:
console.log(3)
可以參考這裡的在線示例。
Angular 自身的代碼(及編譯器生成的代碼)提供了對 Closure Compiler 的 ADVANCED
模式的兼容性保證,因此可以利用 Closure Compiler 來大程度優化編譯體積。
5. What Angular is doing with Bazel and Closure。
去除 Zone.js
曾經的 AngularJS 中,為了保證框架能夠知曉用戶的非同步操作,所有操作都需要使用 $scope.$apply
進行包裝,例如通過 $timeout
、$interval
執行延時任務,從而能夠觸發 AngularJS 的變化檢測。
而 Angular 為了改變這一現狀,引入了 Zone.js 來解決這一問題,通過攔截所有可能的非同步任務觸發過程,從而能夠知曉所有非同步回調的發生。因此,在 Angular 中可以無需任何額外配置的情況下使用純命令式的操作來修改 ViewModel。
在 Angular v4 及之前的版本中,我們需要提供一個什麼都不做的 Zone 對象來規避 Angular 的依賴檢查。不過從 v5 開始,Angular 自身提供了不使用 Zone.js 的支持[6],僅僅需要在啟動代碼中配置 { ngZone: "noop" }
,因此並不需要太擔心兼容性部分,僅僅考慮業務上的實現即可。
所以,為了不用 Zone.js,我們只需要:
- 不使用自動觸發的變化檢測;
- 不使用命令式的操作。
對於 1),我們可以使用 ChangeDetectorRef
API 來手動觸發變化檢測。但對於大型應用而言,難免會增加應用的複雜度。
所以更可行的方式是 2),可以像某些其它框架一樣,放棄命令式的狀態修改,全部使用方法調用的方式來修改數據。雖然事實上仍然是手動觸發 trigger,但只要美名其曰響應式,一切就會順其自然。例如在 Angular No-Zone setState Demo 中,簡單地實現了一個具備批處理能力的 setState[7] 方法,可以在不使用 Zone.js 的情況下較為自然地實現變化檢測。
另一個更適用於 Angular 的實現方式是利用 Observable,由於 Angular 本身具備對 RxJS 的良好集成,引入 Observable 並不需要任何額外的成本。而 Observable 基於事件流的方式工作,只要把內容更新託管給 Pipe,那麼也可以完全規避命令式操作。例如在 Angular No-Zone Observable Demo 中就有使用自定義 Pipe 來自動應用 Observable 狀態更新的例子。
6. 【SNF-A】Angular 增加不使用 Zone.js 的支持。
7. 事實上 Dart 版本的 Angular 原生提供了 setState 方法:angular/component_state.dart at 51cf8625ad35d09f349dc9cac40cd983bd1274d4 · dart-lang/angular,這裡僅針對 JavaScript 版本。
去除 BrowserModule
Angular 自身是一個平台無關的數據綁定框架,為此如果需要讓 Angular 在瀏覽器中運行,需要引入瀏覽器平台相關的部分,即 @angular/platform-browser
。其中具備兩個重要類型,一個是 platformBrowser
,另一個是 BrowserModule
。platformBrowser
是一個 PlatformFactory
,其中包含了一系列預製的基礎 Provider
;而 BrowserModule
是一個 NgModule
,通常由應用根模塊導入,除了包含了一些 Provider
外,也導入了另一些其它的 NgModule
。
由於 Angular 良好的工程化特性,真正實現了模塊間的解耦,因此我們完全可以不引入 BrowserModule
,而是自行提供其中的必要部分。
最終[8],我們僅僅需要自行實現一個 Renderer
,使用不到一百行的代碼就能完全規避對 BrowserModule
的導入,避免引入其它無用部分。
8. 該部分完成過程可以參考 按官方說法,angular2是基於Web組件的開發平台,為什麼卻把它當做前端框架來用? - Trotyl Yu的回答 - 知乎。
去除 Debug Helpers
在 Angular 中,我們通過 enableProdMode
API 來進入生產模式,關閉不必要的調試功能,提供應用性能。不過,由於該方法通過修改模塊局部變數來保存狀態,所以即便是在生產模式中,調試相關的代碼仍然無法被去除[9],即使是 Closure Compiler 這樣恐怖如斯的斗宗強者也無能為力。
而對於查詢應用是否在生產模式,是通過 isDevMode
API 來進行的,不僅是用戶,內部判斷也同樣是通過這個。為此,我們只需要將 isDevMode
修改為 return false
,即可切斷調試服務與內部狀態間的依賴,使其被成功去除。
9. 問題記錄見 [angular/core] make DebugServices treeshakable (~10KB savings)。由於 v6 版本中使用了全新的渲染引擎,將使用全局變數來判斷 devMode,因此能夠原生支持大部分構建工具的優化,所以最終可能並不需要通過 Build Optimizer 來解決。
通過多種優化的組合,我們便可以將 Todo MVC 的應用大小控制在 30KB 左右,在絕大多數 3G 甚至部分 2G 網路上都能立即載入。
當然,實際工程實踐中並不是所有這些優化都有必要(部分固定大小的優化項,隨著應用自身大小的增大可能不再作為瓶頸)。
完整的 Demo 可以參見 trotyl/ng-slim-demo: Count Angular project size for Todo MVC with different optimization。
本文地址:https://zhuanlan.zhihu.com/p/32137489
推薦閱讀: