Ionic/Angularのパフォーマンスチューニング

Ionic 4はパフォーマンスを意識した構成となっており、様々な要素からパフォーマンスチューニングを行うことができます。本記事では、Ionic 4のStarter Template（@ionic/angular 4.5.0）をベースとしてパフォーマンスチューニングを行います。

この内容は7月7日のIonic Meetupでの登壇内容です。
https://ionic-jp.connpass.com/event/133896/

> Note: `Eager Loading` は、ModuleをLazy Loadingしないことです。

## 結論
### 初期表示について
- ModuleのEager Loadingにより高速化します
- Ionic ComponentsのPreLoadにより高速化します
- Service WorkerのPreFetchは影響がありません

### ページ遷移について
- ModuleをPreLoadによって高速化します
- Ionic ComponentsのPreLoadによって高速化します
- Service WorkerのPreFetchにより高速化の可能性あり

### 未検証
- rel="preload"によるIonIconの先読み

## 測定手法
### サーバ環境
- サーバは高機能無料ホスティングサービス「Netlify」
- Netlifyのサブドメイン
- Asset optimizationは `Disable`
- HTTP/2 Server Pushは未使用

### 測定
#### 合成モニタリング
[株式会社Spelldata](https://spelldata.co.jp) 様にご協力いただき、 `Catchpoint` を使って測定を行いました。Netilyのサーバに近いサンフランシスコを計測ノードに設定。

https://spelldata.co.jp/catchpoint/

- 48時間計測（2019年6月19日12:00から48時間）
- 計測値　サンフランシスコ
- n=576 × 3ドメイン
- iPhone7 - 4G Emulate
- 単位： `ms`
- https://qiita.com/rdlabo/private/d7b4bcbc0886f0175926 で検証結果をまとめています。

#### 簡易計測
記事執筆用に、Google Chromeの標準機能についているAudits(Lighthouse)で測定後、 `trace` より `Performance` を確認し、そのtime及びレンダリング結果を利用しています。レンダリングがブロックされているなど明らかに差がある場合はこちらを利用しています。

## 仮説（高速化手法）
### 1. ModuleをEager Loadingすると速くなる
Angularでは、Module単位でLazy Loadingすることができます。

- [参考：フィーチャーモジュールの遅延ロード](https://angular.jp/guide/lazy-loading-ngmodules)

これにより初期bundleサイズを小さくすることができますので、規模の大きいアプリケーションの高速化に有効です。
反面、JavaScriptファイルを複数呼び出すことになりますので、初期表示に必要なModuleをLazy Loadingすると呼び出しが増えるので遅延している可能性があります。

### 2. Ionic ComponentsのPreLoad
Ionic ComponentsはすべてLazy Loadingで呼び出すようになっており、呼び出しのタイミングはHTMLファイルもしくはHTMLテンプレートがparseされたタイミングとなります。

ですので、 `display: none;` で表示せず先にDOMを構成しておけば、PreLoadすることができます。

```index.html
  <section style="display: none;">
    <ion-tabs><ion-tab-bar><ion-tab-button></ion-tab-button></ion-tab-bar></ion-tabs>
    <ion-header><ion-toolbar><ion-title></ion-title></ion-toolbar></ion-header>
    <ion-content></ion-content>
    <ion-card><ion-card-header></ion-card-header><ion-card-content></ion-card-content></ion-card>
    <ion-list><ion-list-header><ion-item></ion-item></ion-list-header></ion-list>
    <ion-label></ion-label>
  </section>
```

このことによって高速化する可能性があります。

### 3.  Service WorkerのPreFetch
`@angular/pwa` を利用することによって、デフォルトでJavaScriptファイルをPreFetchすることができます。

```ng-sw.config.json
  "assetGroups": [
    {
      "name": "app",
      "installMode": "prefetch",
      "resources": {
        "files": [
          "/favicon.ico",
          "/index.html",
          "/*.css",
          "/*.js"
        ]
      }
...
```

このことによって高速化する可能性があります。


## 検証
### 初期表示の検証
Starter Templateの `Tabs` を使って検証しました。ソースコードは https://github.com/ionic-jp/tabs-compare で公開しています。

#### 1. ModuleをEagerLoadingにする
https://github.com/ionic-jp/tabs-compare/blob/no-module-lazy/src/app/app-routing.module.ts#L9-L17

__効果がありました。__
初期テンプレートのままだと、 `Tabs` の初期表示は2つのModuleをLazy Loadingする必要があります。

```
`app.module.ts` => `tabs.module.ts` => `tab1.module.ts`
```

これをすべて `app.module.ts` に統合して `tabs.module.ts` 、 `tab1.module.ts` を削除しました。

| ドメイン | Mdn名前解決 | Mdn レンダリング開始 |
|:--|:--|:--|
| Default | 7.00 | 697.00 |
| EagerLoading | 7.00 | 683.50(-13.5) |

レンダリング開始時間にわずかながら差を確認することができます。また、スピードインデックスにおいては、99パーセンタイルまですべてEagerLoadingがDefaultを速度で上回っていました。

![スクリーンショット 2019-06-21 15.04.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100460/89c9f2b0-44ee-2363-6c0c-55c23cd92c0a.png)


#### 2. Ionic ComponentsのPreLoad
https://github.com/ionic-jp/tabs-compare/blob/no-module-lazy/src/index.html#L24-L32

__効果がありました。__

計測上、Defaultを `EagerLoading & PreLoad` と比較していますが、EagerLoadingのみの時よりも高速化していることがわかります。

| ドメイン | Mdn名前解決 | Mdn レンダリング開始 |
|:--|:--|:--|
| Default | 7.00 | 697.00 |
| EagerLoading | 7.00 | 683.50(-13.5) |
| EagerLoading & PreLoad | 7.00 | 637.50(-59.5) |

スピードインデックスにおいては、99パーセンタイルまですべてEagerLoading & PreLoadがDefault、EagerLoadingを速度で上回っていました。

![スクリーンショット 2019-06-21 14.46.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100460/afaffa76-1a01-c5d3-ecb9-9db0f412b42b.png)


#### 3.  Service WorkerのPreFetch
https://github.com/ionic-jp/tabs-compare/blob/total/src/ngsw-config.json#L10-L12
__効果はありませんでした__

Service WorkerのPreFetchは、 `First meaningful Paint` 終了後に実行されます。そのため、Service Workerを有効にしても初期表示への影響はありません。

### ページ遷移の検証
#### 1. ModuleをPreLoadする
https://github.com/ionic-jp/blank-preload/blob/preload/src/app/app-routing.module.ts#L12
__効果がありました。__

遷移先のページをEagerLoadingにすることにより、ページ遷移時の待機時間が削減されます。ただし、冒頭に書いた通りLazy Loadingしないことによりbundleサイズが大きくなるので、初期表示が遅くなる可能性があります。
そのため、 `preloadingStrategy` を `PreloadAllModules` に設定しておき、遷移前にLoadを完了しておく必要があります。

```app-routing.module.ts
@NgModule({
  imports: [
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
  ],
  exports: [RouterModule]
})
```

ただし、 `PreloadAllModules` は事前にLoadするModuleを選択することができませんので、Moduleが多い場合はPreloadするModuleを指定する方が確実にページ遷移の待ち時間を削減することができます。

詳細は [Preloading modules in Ionic v4](https://medium.com/@david.dalbusco/preloading-modules-in-ionic-v4-1839c34384fb) を参照ください。

#### 2. Ionic ComponentsのPreLoad
https://github.com/ionic-jp/blank-preload/blob/preload/src/app/home/home.page.ts#L12-L21
__効果がありました。__

しかし、Ionic ComponentsのLoad中は `ion-router-outlet` が遷移をブロックしますので、 `home.html` テンプレートに直接書くと `/home`の表示が遅くなってしまいます。ですので、TSで非同期にDOMを構築して、PreLoadを行います。

```home.page.ts
  ionViewDidEnter() {
    const preloadArea: HTMLElement = document.getElementById('preload');
    preloadArea.appendChild(document.createElement('ion-card'));
    preloadArea.appendChild(document.createElement('ion-card-header'));
    preloadArea.appendChild(document.createElement('ion-card-subtitle'));
    preloadArea.appendChild(document.createElement('ion-card-title'));
    preloadArea.appendChild(document.createElement('ion-card-content'));
    preloadArea.appendChild(document.createElement('ion-back-button'));
  }
```

上のgifアニメーションがPreloadなし、下がPreloadありとなります。わかりやすいように遷移アニメーションは無効にしています。

![blank-default.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100460/9f87b0da-3e84-21ca-8d12-6f7d5a989845.gif)
デモ：https://blank-default.netlify.com

![blank-preload.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100460/e775651d-d666-eba8-a490-ec5a721838ce.gif)
デモ：https://blank-preload.netlify.com

上のgifアニメーションでは、buttonクリック後、2つのJavaScriptファイルを読み込むまで待機しているのに対し、下ではLoad済みなのでクリック直後に遷移していることがわかります。
また、遷移後のIonBackButtonの表示が遅延していないことも確認できます。


#### 3.  Service WorkerのPreFetch
__効果はある場合があります__

しかし、下り30Mbps環境で22000ms程度が必要になります。そのため、ユーザが長時間アプリを操作する場合は、PreFetchによる高速化を期待することができます。

![blank-sw.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100460/40f68d7e-aaa7-9f81-4478-a42c63f3d821.gif)
デモ：https://blank-sw.netlify.com/

すべてのファイルのロードに時間がかかるので、オフラインキャッシュの副次的な効果程度に留めておくのがいいでしょう。

### 未検証だが効果がある可能性の高い施策
#### rel="preload"によるIconの先読み
`ionIcon` は、Web ComponentsのRender後に該当するSVGファイルを呼び出し、表示します。
参考コード：https://github.com/ionic-team/ionicons/blob/master/src/components/icon/icon.tsx#L206

そのため、Render以前に利用するSVGファイルを呼び出すると高速化に効果がある可能性があります。

<img width="492" alt="スクリーンショット 2019-06-18 19.39.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100460/1e6ee920-f497-9932-c1b3-6ce69c0388ec.png">
（アイコンが表示されないため、Visually Loadedされない）

当初そのように思い、利用する `ionIcon`　もすべて `display:none` を使ってpreLoadを試しましたが、高速化しませんでした。

デモ：https://tabs-bad-lazy.netlify.com

SVGファイルの呼び出し完了までブロックが発生しているようで異なるアプローチが必要であり、 `rel="preload"` はそのひとつとして有力です。
iOS/Androidの判定をどうするかといった問題がありますので未検証のままですが、高速化の際にご留意いただけましたら幸いです。

`ionImg` についても同様です。


## まとめ
パフォーマンスチューニングには様々なアプローチがありますが、フロントエンドのエンジニアリングのみで行う場合は「ファイルのリクエストを操作する」、「レンダリングブロックを排除する」のふたつが大きなウェイトを占めることになります。
Developerツールをみながら様々な仮説を立て、検証いただければと思います。

なお、検証結果で差がわずかな場合は、自分自身のネットワーク環境に依存しながら「都合のいい結果」を得ている可能性がありますので、そのような思い込みを排除するためにツールを使った定点観測はとても有用です。

今回、[株式会社Spelldata](https://spelldata.co.jp)様にご協力いただき利用した [Catchpoint](https://spelldata.co.jp/catchpoint/) は、SPA/PWAを正しく計測できるすばらしいツールですので、ぜひ計測を行う際はご利用をご検討ください。

それでは、また。