AppShellモデルを適用してmain.bundleのサイズ削減する

# 試したこと

- AppShellモデルでモジュール構成を組み直す。[^1]
- そのために共有リソースをCoreModuleとSharedModuleに分割する。[^2]
- main.bundleのファイルサイズ削減を計測する。

# 環境

- Angular:5.2.6
- Angular-CLI:1.7.1

# モジュール構成の戦略

## AppModuleをAppShellモデルにする

<img width="752" alt="スクリーンショット 2018-03-12 0.07.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/194428/2e674774-ae19-3d52-0519-8f4af83da8a8.png">


AppShellモデルのために、AppModuleは以下の役目だけを持たせます。

- application shell
- 各content（LazyLoadingModule）へのルーティング
- DIコンテナの最上位ルート

これにより初回ロードに必要なmain.bundleは、AppShellだけになります。
アプリ全体だと次のようになります。

<img width="506" alt="スクリーンショット 2018-03-13 15.25.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/194428/6e1a36b8-3c16-a27c-c099-3453564f2d6a.png">

各contentはLazyLoadingModuleへ回して、できるだけ共有リソースもAppModuleから外すことで、ユーザー作成ソースだけでなくベンダー系ソースも後送りにします。結果を見ると、ベンダー系ソースではAngularMaterialの効果が絶大です。

以上の変更によりmain.bundleがサイズ削減されて起動時間が短縮します。

## 元の構成

<img width="709" alt="スクリーンショット 2018-03-11 22.22.22.png" src="https://qiita-image-store.s3.amazonaws.com/0/194428/e2d5b2e1-946c-1af6-0679-74fda4e312e8.png">

- 共有リソースをSharedModuleに全て突っ込んでいた
- SharedModuleをAppModule含めたあらゆるモジュールでインポートする
- 最上位のAppModuleで`forRoot()`によるシングルトンサービスを生成する
- Rxは`rxjs/add/`からインポート

## 新しい構成

<img width="712" alt="スクリーンショット 2018-03-11 22.19.46.png" src="https://qiita-image-store.s3.amazonaws.com/0/194428/b73adafe-f6c9-b52e-d4a0-fe5aa5adc364.png">

- SharedModuleとCoreModuleに分ける
- AppModuleにCoreModuleをインポートする
- 各LazyLoadingModuleにSharedModuleをインポートする
- Rxをpipe化する

## CoreModuleとSharedModuleの分け方

### CoreModule
- ナビゲーション系component
- ナビゲーション系componentで使うdirective/pipe、materialモジュール
- アプリ全体で使用するAngularパッケージ
    - BrowserAnimationsModule
    - HttpClientModule
    - ServiceWorkerModule
- アプリ全体で使用するservice
- app-routingで使用するguard

### SharedModule
- フィーチャーモジュールで使用するAngularパッケージ
    - CommonModule
    - FormsModule/ReactiveFormsModule
- フィーチャーモジュールで使用する共有リソース
    - directive
    - pipe
    - guard
    - materialモジュール

共有serviceは全てCoreModuleに入れています。特定のフィーチャーモジュールだけで使うserviceなら特定のフィーチャーモジュールでprovideします（SharedModuleには入れません）。複数モジュールに跨るservideをSharedModuleに入れてしまうと、フィーチャーモジュール毎にコピーを生成します。そうなるとアプリ全体で見た時にシングルトンのサービスにならないため、データの共有できないことに注意です。

# バンドルファイルのサイズ変化

## サマリー

- 後送りにした共有リソース分がmain.bundleからcommon.chunkに移動する。
- `--prod`ビルドのmain.bundleは、`--aot`ビルドでのmain.bundleとvendor.bundleで構成されるが、`--aot`ビルドでは以下の変化となる。
    - SharedModuleに回したユーザー作成リソースがmain.bundleからcommon.chunkに移動する。
    - Angularパッケージ系（FormsModuleやAngularMaterial）のソースがvendor.bundleからcommon.chunkに移動する。
    - RxJSはvendor.bundleのままで移動しない。これはvendor.bundle内のAngularパッケージが使用しているからだと思われる。
- 微量だが、合計サイズも減った。要因は不明である。

## 集計方法

1. [採取用のサンプルソース](https://github.com/studioTeaTwo/angular-starter/commits/example/reconstruct-module)を作成した。
  - 元の状態はコミット`refactor: divide feature module from app module`[^3]である
  - 新しい状態はコミット`refactor: reconstruct module and reorganize rxjs`[^4]である
  - フィーチャーモジュールが一つのためcommon.chunkは作成されない。demo.chunkがsharedModuleを内包している。
2. 上記2つのコミットに対して、`--aot`ビルドと`--prod`ビルドでそれぞれビルド結果を採取した。

## `--aot`ビルドでの変化

元の構成でのビルド結果

```
Time: 12854ms
chunk {demo.module} demo.module.chunk.js, demo.module.chunk.js.map () 51.4 kB  [rendered]
chunk {inline} inline.bundle.js, inline.bundle.js.map (inline) 5.83 kB [entry] [rendered]
chunk {main} main.bundle.js, main.bundle.js.map (main) 61.4 kB [initial] [rendered]
chunk {polyfills} polyfills.bundle.js, polyfills.bundle.js.map (polyfills) 325 kB [initial] [rendered]
chunk {styles} styles.bundle.js, styles.bundle.js.map (styles) 63.7 kB [initial] [rendered]
chunk {vendor} vendor.bundle.js, vendor.bundle.js.map (vendor) 3.88 MB [initial] [rendered]
```

新しい構成でのビルド結果

```
Time: 12936ms
chunk {demo.module} demo.module.chunk.js, demo.module.chunk.js.map () 2.04 MB  [rendered]
chunk {inline} inline.bundle.js, inline.bundle.js.map (inline) 5.83 kB [entry] [rendered]
chunk {main} main.bundle.js, main.bundle.js.map (main) 30.6 kB [initial] [rendered]
chunk {polyfills} polyfills.bundle.js, polyfills.bundle.js.map (polyfills) 325 kB [initial] [rendered]
chunk {styles} styles.bundle.js, styles.bundle.js.map (styles) 63.7 kB [initial] [rendered]
chunk {vendor} vendor.bundle.js, vendor.bundle.js.map (vendor) 1.88 MB [initial] [rendered]
```

|ファイル|元の構成|新しい構成|
|:--:|--:|--:|
|main.bundle.js|61.4kB|30.6kB|
|vendor.bundle.js|3.88MB|1.88MB|
|demo.module.chunk.js|51.4kB|2.04MB|
|合計|3992.8kB|3950.6kB|

特にvendor.bundleが2MBと大きく移動していることがわかる。今回のサンプルソースでは、ほぼAngularMaterialになる。

## `--prod`ビルドでの変化

元の構成でのビルド結果

```
Time: 28226ms
chunk {0} 0.aa68a6ffd1d68364462f.chunk.js () 8.04 kB  [rendered]
chunk {1} main.212ed5e5d1c0616288fd.bundle.js (main) 563 kB [initial] [rendered]
chunk {2} polyfills.57c88da6a6d2a800f78f.bundle.js (polyfills) 126 kB [initial] [rendered]
chunk {3} styles.60f2c673d6a13f4d5d86.bundle.css (styles) 46.8 kB [initial] [rendered]
chunk {4} inline.735dc2ac6d85cc81d4a0.bundle.js (inline) 1.4 kB [entry] [rendered]
```

新しい構成でのビルド結果

```
Time: 11336ms
chunk {0} 0.aaac215c03d911a8600c.chunk.js () 213 kB  [rendered]
chunk {1} polyfills.6a4c24b5ca42039de4fd.bundle.js (polyfills) 126 kB [initial] [rendered]
chunk {2} main.4a3d80967dfb1e57f705.bundle.js (main) 326 kB [initial] [rendered]
chunk {3} styles.60f2c673d6a13f4d5d86.bundle.css (styles) 46.8 kB [initial] [rendered]
chunk {4} inline.30a8d86ae20b87488c1f.bundle.js (inline) 1.4 kB [entry] [rendered]
```

|ファイル|元の構成|新しい構成|
|:--:|--:|--:|
|main.bundle.js|563kB|326kB|
|0.chunk.js|8.04kB|213kB|
|合計|571.04kB|539kB|

初回ロードで読み込まれるmain.bundleが約2/3に減った。

[^1]: App Shell モデル。https://developers.google.com/web/fundamentals/architecture/app-shell?hl=ja
[^2]: Angularスタイルガイドを参考にしながら進めた。https://angular.io/guide/styleguide#application-structure-and-ngmodules
[^3]: https://github.com/studioTeaTwo/angular-starter/commit/5963e398d1492666c54ea9627849ebb3cc3b0214
[^4]: https://github.com/studioTeaTwo/angular-starter/commit/6f90fdda2ebd0f04bf7dd2510922e393a9ab01dc