Angular 應用瘦身記——比 jQuery 更小的 TodoMVC

TrotylYu發表於2017-12-18

本文內容提取自 《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. 不使用自動觸發的變化檢測;
  2. 不使用命令式的操作。

對於 1),我們可以使用 ChangeDetectorRef API 來手動觸發變化檢測。但對於大型應用而言,難免會增加應用的複雜度。

所以更可行的方式是 2),可以像某些其它框架一樣,放棄命令式的狀態修改,全部使用方法呼叫的方式來修改資料。雖然事實上仍然是手動觸發 trigger,但只要美名其曰響應式,一切就會順其自然。例如在 Angular No-Zone setState Demo 中,簡單地實現了一個具備批處理能力的 setState7 方法,可以在不使用 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,另一個是 BrowserModuleplatformBrowser 是一個 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

本文地址:juejin.im/post/5a3776…

相關文章