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

本文內容提取自 《2017成都WEB前端交流大會》 中的主題演講。javascript

衆所周知,Angular 以官方提供的一體化解決方案(全家桶)而聞名,官方團隊提供了構建 Web App 所需的大部分類庫和工具支持。html

不過,「大而全」的「大」指的是覆蓋範圍廣,而並不是應用體積大,這裏咱們以 TodoMVC 爲例,將其優化到比 jQuery 更小的體積1前端

應用加載大小示例

1. 本文寫做時 jQuery 的最新版本爲 3.2.1,非 slim 版本 min+gzip 後大小爲 33,861B。爲 Chrome 中的實際傳輸大小,可能與本地壓縮結果略有差別。java

AOT 編譯

Angular 模版採用了編譯到 JavaScript 結構化數據的方式2,雖然組件模版使用 .html 文件3定義,但瀏覽器並不能見到這個 HTML 文件,而只能見到編譯後的 JavaScript 文件。git

例如,一個簡單的表單控件:github

<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>
複製代碼

將會被編譯爲4web

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 編譯。api

因爲 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…

相關文章
相關標籤/搜索