[Angular] ビルド後のファイルの出力サイズを試行錯誤しつつ減らしてng buildのコマンドオプションを最適化する方法を考える


2020/04/18

Angularプロジェクトをビルドを最適化する際に、ビルドオプション
--build-optimizer=true --aot=trueを利用されておられるでしょうか。

Angularのビルドオプション自体は
angular.json(旧angular-cli.json)のconfigurationsタグに記述されています。

通常は意識せずにAngularアプリをビルドできているのはこのためですが、このビルドオプションは直接コマンド引数として渡すこともできます。

今回は
このサイトに感化され、思いのままに書き殴ったまま肥大化していった現状のAngularプロジェクトをそろそろメタボ解消しようと思い立ちました。

とりあえず、Brotli などの圧縮してれるライブラリは使わず、無駄にimportしまくっているモジュールを削ったり、最適化をします。


TL;DR

今回の記事は、Angular(執筆段階ではangular-cliはver.8)のコンパイラーはかなり優秀です。

プログラマーが意識して積極的にコード最適化する必要はないと言うことを懐疑的に検証してみた内容となっております。

結論としては、苦労して小技を繰り出すことで数十kBの程度はスリム化出来る可能性があるかもしれませんが、コスト対効果を考えると暗黙的にコンパイラーにお任せしといた方が幸せになれるはずです。

個人的にビルドオプションを弄った限りで一番スリムでサイズも出力もスッキリしたのが、

            
            $ ng b --prod --build-optimizer=true --aot=true --output-hashing=none
        
辺りが最も出力サイズが抑えられると思います。

では以下、具体的なビルドサイズの軽減のための実験の内容を一ステップづつ継ぎ足しながら検証してみます。


ng build 後の生成物に付くハッシュ値を消す方法

まず最初に、直接はビルド後の出力ファイルサイズを低減に関係はないですが、主力されるファイルについてまわるハッシュ値の消し方を説明しましょう。

Angular Project の設定そのままでprodフラグでビルドすると、出力させるファイルには、それぞれハッシュ値が以下のように自動で付与されます。

            
            $ ng build --prod --build-optimizer true --aot true

Date: 2019-06-24T07:45:43.041Z
Hash: 4470e19ad1e0b3dfa8cb
Time: 112933ms
chunk {0} runtime.26209474bfa8dc87a77c.js (runtime) 1.41 kB [entry] [rendered]
chunk {1} es2015-polyfills.bda95d5896422d031328.js (es2015-polyfills) 56.6 kB [initial] [rendered]
chunk {2} main.a89343786a3a7a06abc2.js (main) 1.24 MB [initial] [rendered]
chunk {3} polyfills.7dc9f29e95cefa5190c3.js (polyfills) 41 kB [initial] [rendered]
chunk {4} styles.602cfcb7b605501de325.css (styles) 84.8 kB [initial] [rendered]
        
このハッシュ値は、例えば複数人で開発するなど、ビルドバーションをキチンと管理することを要求されている場合には便利ではあります。

個人でしか更新しないようなプロジェクトでは、むしろ無い方がスッキリしていいんじゃ無いかと個人的におもいます。

そんな時には、
--output-hasing noneオプションを付けましょう。

            
            $ ng build --prod --build-optimizer true --aot true --output-hashing none

Date: 2019-06-24T08:33:07.003Z
Hash: 5125e69c6cc20452a05c
Time: 67533ms
chunk {0} runtime.js (runtime) 1.41 kB [entry] [rendered]
chunk {1} es2015-polyfills.js (es2015-polyfills) 56.6 kB [initial] [rendered]
chunk {2} main.js (main) 1.24 MB [initial] [rendered]
chunk {3} polyfills.js (polyfills) 41 kB [initial] [rendered]
chunk {4} styles.css (styles) 84.8 kB [initial] [rendered]
        
心なしか、ハッシュ値を与えないぶん、ビルド時間が早くなったような…たぶん気のせいでしょう。


ソースコードまわりを手動でスリム化

無駄だとは分かっていても、ソースコード内で使われていないまま放置されているライブラリなどをお手手で消して回ります。

個人的にコンパイラとかの予備知識など全くなくお恥ずかしいことですが、ソースコードに散らばった
はぐれライブラリたちは最適化の最中にコンパイラが自動で読み込まない・スキップするなどの対処を自動で行ってくれるはずです。

結果は分かっているのですが、とりあえず実験ということで、無駄に放置したリソースを見つけて手動削除を繰り返します。

削除対象1: インポートしながらも未使用のライブラリ

例えば手元のapp.module.tsの中身に、

            
            import { HighlightModule, HIGHLIGHT_OPTIONS } from 'ngx-highlightjs';
                          ^^^^^^^^^^^^^^^^^
// どこにも使われて無いまま残るやつ
        
みたいな、日頃から整理するのも面倒で放置したものがたくさんあります。

こいつらを根こそぎインポートのスコープから削除し、ビルドをしてみると…

            
            chunk {0} runtime.js (runtime) 1.41 kB [entry] [rendered]
chunk {1} es2015-polyfills.js (es2015-polyfills) 56.6 kB [initial] [rendered]
chunk {2} main.js (main) 1.24 MB [initial] [rendered]
chunk {3} polyfills.js (polyfills) 41 kB [initial] [rendered]
chunk {4} styles.css (styles) 84.1 kB [initial] [rendered]
        
出力結果に変わる筈もなく…次いってみましょう。

削除対象2: 無駄なコンポーネント

もしもの為に、サードパーティのjwtを受け取るように作っていたCallbackコンポーネントをしばらく使う予定が無いので、この際バッサリ捨てます。

            
            // import { CallbackComponent } from './components/callback/callback.component';

@NgModule({
    declarations: [
        AppComponent,
        HeaderToolbarComponent,
        // CallbackComponent,
        // ...
        
Routerからも弾きます。

            
            // ...
// import { CallbackComponent } from './components/callback/callback.component';

const routes: Routes = [
    // ...
    // {
    //     path: 'callback',
    //     component: CallbackComponent,
    // },
        
どうやらCallbackコンポーネントごと消さないとビルド出来ないようで、根こそぎ削除して、

            
            chunk {0} runtime.js (runtime) 1.41 kB [entry] [rendered]
chunk {1} es2015-polyfills.js (es2015-polyfills) 56.6 kB [initial] [rendered]
chunk {2} main.js (main) 1.24 MB [initial] [rendered]
chunk {3} polyfills.js (polyfills) 41 kB [initial] [rendered]
chunk {4} styles.css (styles) 84.1 kB [initial] [rendered]
        
どうやら、中身カスカスのコンポーネントは、いてもいなくてもサイズ変わりません!

削除対象3: 無駄にクラスに実装しているインターフェイス

ここもかなりのリソースの無駄づかいをしております…

まず、あとで使うつもりであった
OnInitOnDestoryAfterViewInitなどを実装していたり、'base'とid指定してたhtml要素が既になかったり…

例えば、以下のように、何もさせない
AfterViewInitを実装させているソースコードなどです。

            
            import { Component, OnInit, AfterViewInit } from '@angular/core';
...
@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit, AfterViewInit {
    title = 'tacosKingdom';
    opened: boolean;

    constructor(
        @Inject(PLATFORM_ID) private platformId: any,
        @Inject(DOCUMENT) private document: any
    ) {
    }

    public ngOnInit(): void {
        if (!isPlatformBrowser(this.platformId)) {
            const bases = this.document.getElementsByTagName('base');
            if (bases.length > 0) {
                bases[0].setAttribute('href', environment.baseHref);
            }
        }
    }

    public ngAfterViewInit() {}

    public innerToggle() {}
}
        
とにかく使ってないものは剥ぎとります。

            
            chunk {0} runtime.js (runtime) 1.41 kB [entry] [rendered]
chunk {1} es2015-polyfills.js (es2015-polyfills) 56.6 kB [initial] [rendered]
chunk {2} main.js (main) 1.24 MB [initial] [rendered]
chunk {3} polyfills.js (polyfills) 41 kB [initial] [rendered]
chunk {4} styles.css (styles) 84.1 kB [initial] [rendered]
        
...変化なし!

ここまで記事を読んでただいて、申し訳ないですが、なんと生半可にコードに残ったゴミを消しただけでは、何も変わらないということが分かりました…

逆に言えば、この程度の修正でできる最適化って、コンパイラがほぼほぼ全て全てやってくれてるんですね。

次に巨大なライブラリとされる 
@angular/core @angular/common rxjs などが、無駄にサブモジュール等でインポートされてはいないかチェックしこうと思います。

削除対象4: 重い外部ライブラリ(Angular Materialなど)

Angular Materialはキレイな見た目のカスタムHTMLコンポーネントがお手軽に利用できて便利です。

一方、デメリットとして、モジュールを読み込めば読み込むほど、最終的なビルドサイズが肥大化していきます。

そんな
Angular Materialですが、気づくと使わないモジュールもガンガン追加しちゃってました...。

使わないコンポーネントモジュールは消していきます。

            
            import {
    // MatSidenavModule, <-- Useless
    // MatCheckboxModule, <-- Useless
    MatButtonModule,
    MatIconModule,
    MatCardModule,
    MatFormFieldModule,
    MatInputModule,
    MatToolbarModule,
    // MatDialogModule, <-- Useless
    // MatGridListModule, <-- Useless
    MatListModule
} from '@angular/material';

@NgModule({
    ...
    imports: [
        ...
        MatButtonModule,
        // MatCheckboxModule, <-- Useless
        // MatDialogModule, <-- Useless
        MatCardModule,
        // MatSidenavModule, <-- Useless
        MatIconModule,
        MatFormFieldModule,
        MatInputModule,
        MatToolbarModule,
        // MatGridListModule, <-- Useless
        MatListModule,
        
結果は以下でmain.jsは0.06MB減量しました。

            
            chunk {0} runtime.js (runtime) 1.41 kB [entry] [rendered]
chunk {1} es2015-polyfills.js (es2015-polyfills) 56.6 kB [initial] [rendered]
chunk {2} main.js (main) 1.18 MB [initial] [rendered]
chunk {3} polyfills.js (polyfills) 41 kB [initial] [rendered]
chunk {4} styles.css (styles) 84.1 kB [initial] [rendered]
        

削除対象5: 重い標準ライブラリ(ReactiveFormsModuleなど)

Angular標準のモジュールなどでも、ReactiveFormsModuleなどのように、実際は全く使ってないモジュールをインポートしたまま放置している場合があります。

そんな無駄なモジュールがリソースを食っているかも...と疑って、これらを必要なモジュール以外削除することにしました。

            
            import { ReactiveFormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
...
@NgModule({
    ...
    imports: [
        ...
        // BrowserAnimationsModule, <-- Useless
        // ReactiveFormsModule, <-- Useless
        // HttpClientModule, <-- Useless
        ...
        
その結果、

            
            chunk {0} runtime.js (runtime) 1.41 kB [entry] [rendered]
chunk {1} es2015-polyfills.js (es2015-polyfills) 56.6 kB [initial] [rendered]
chunk {2} main.js (main) 1.18 MB [initial] [rendered]
chunk {3} polyfills.js (polyfills) 41 kB [initial] [rendered]
chunk {4} styles.css (styles) 84.1 kB [initial] [rendered]
        
プロジェクトのコンポーネント、サービス、モジュールを綺麗にしたところで、全くスリム化する気配なし!

なんと全然…変わりませんでした…

これらの脇役モジュールって、コンパイラが賢く圧縮していたのでしょうか。

以上手作業でのダイエットは、ここいらで潮時かもしれません。

コンパイラの有能さ恐るべし。


【おまけ】 --optimizationオプションに関して

公式のCLIリファレンスをみるとbuildコマンドの最適化オプションには2つ存在しています。

            
            --buildOptimizer:
    --aotオプションが有効化していた場合に、
    内部の@angular-devkit/build-optimizerを利用して最適化させます

--optimization:
    ビルド生成物に対し最適化させます
        
安直に考えると、両方ビルドオプションに指定しとけば、ビルド後の生成コードが最適化されるのでは...と期待しておりました。

実際やってみると
buildOptimizerで最適化した後に、更にoptimizationで最適化してもほとんど効果が出ませんでした。

やるならどちらか一つの最適化オプションを試すべきです。

ちなみに
buildOptimizeraotコンパイラが、ソースコードをコンパイル実行中に走るタイプの最適化をしています。

一方で、
optimizationwebpackがビルド後の生成物をバンドルする際に内部でbabelで最適化を実行している違いがあるようです。

いずれにせよ、
--aot=true --buildOptimizer=trueで最適化は事足りると言えます。


結論

Angularのコンパイラは、相当賢いので、ほぼお任せでも大分スリム。

別方向の最適化でファイル圧縮などがあります。

なおファイル圧縮に関しては別記事ではありますが、
Brotliによる圧縮を試みた記事も興味があればご参考にどうぞ。


参考

Tips to Reduce Angular App Size

CLI - ng build