Help us understand the problem. What is going on with this article?

Angular+Express.jsプロジェクトの高速化メモ

More than 1 year has passed since last update.

Angular+Express.jsで構築したプロジェクトで、初回表示時に10秒前後かかってしまっていた問題を解決するために対応したことをメモっておこうと思います。

ちなみに、改善前はmain~.jsが1.5MBくらいあったのが、400MBくらいまで減らすことに成功し、初回起動時間も3秒程度まで短くなりました:smile:

Compressionを使ってGZIPに圧縮する

Express.jsやNest.jsを使ってるなら必須で入れるべき対応かと思います。
今回の対応の中で一番効果がありました!!

npm install --save compression
app.js
const app = express();

app.use(compression({level: 6}));

圧縮レベルも指定できるみたいです。
level:6で1.5MB⇒400MBくらいまで減りました。

圧縮が効いてると、Response HeaderでContent-Encoding: gzipが指定されるようになります。

遅延ロードを活用する

遅延ロードは対象のモジュールを使うときにはじめてコードを読み込むというものです。
ビルドしたときにjsファイルが分割されて作成され、対象の画面を表示する際に分割したjsファイルを読みます。

app-routing.module.ts
const routes: Routes = [
    { path: '', loadChildren: () => import('./child/child.module').then(m => m.ChildModule) }
]
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Angular8からは動的インポートが使えるようになりました。

サービスの@InjectableprovidedIn: 'root'を使わない

サービスのリファレンスを見ると、以下のように@InjectableprovidedIn: 'root'を設定していたので、何も疑わずにマネしてました。
ただ、これやるとmain~.jsに取り込まれるため、あるComponentでしか使わないようなサービスで指定してしまうとかなり無駄です。。。

/src/app/message.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class MessageService {
  messages: string[] = [];

  add(message: string) {
    this.messages.push(message);
  }

  clear() {
    this.messages = [];
  }
}

対策

全画面共通のサービス以外は@InjectableprovidedInを指定しない。
遅延ロードを使っている場合、子Moduleの@NgModule内でprovidersに指定することで利用個所を限定しましょう。
スタイルガイドの全体構造のガイドラインに書いてあるようなディレクトリ構造できっちりサービスやモジュールの範囲を限定できると良いかな?と思います。

ShareModuleはよく検討する

NgxBootstrapをつかってたんですが、何を思ったのかShareModuleを一つ作成し、その中にNgxBootstrapの各部品Moduleをインポートし、すべてのModuleでShareModuleをインポートしてました。
なんのためにModuleが分割されてるのか。。。

不要なModuleをインポートしてしまうので、jsサイズが大きくなってしまいます。
むやみにShareModuleを作成しないようにしましょう:sweat_smile:

外部のcssはCDNを使う

PrimeNGのように、angular.jsonでcss取り込みをするように書かれているパッケージは多いと思います。
ただ、使っていないスタイルも取り込まれてしまうので、styles~.cssのサイズが数百kBレベルで大きくなります。

そこで、cssをプロジェクト内に取り込まず、CDN経由で読み込むことでstyles~.cssのサイズが大きくなるのを避けます。

PrimeNGで試してみる

cssをプロジェクト内に取り込む場合

まずはスタートガイド通りにangular.jsonでcssファイルの指定をします。

angular.json
            "styles": [
              "src/styles.scss",
              "node_modules/primeicons/primeicons.css",
              "node_modules/primeng/resources/themes/nova-light/theme.css",
              "node_modules/primeng/resources/primeng.min.css",
            ],
$ ng build --prod --aot

Date: 2019-08-01T23:40:34.679Z
Hash: 9a4ed634ff38e222f351
Time: 16220ms
chunk {0} runtime-es5.741402d1d47331ce975c.js (runtime) 1.41 kB [entry] [rendered]
chunk {1} main-es5.3c79bd2946c435722b96.js (main) 242 kB [initial] [rendered]
chunk {2} polyfills-es5.7f43b971448d2fb49202.js (polyfills) 111 kB [initial] [rendered]

Date: 2019-08-01T23:40:47.038Z
Hash: 0c5b82dc37f1d83ef447
Time: 12328ms
chunk {0} runtime-es2015.858f8dd898b75fe86926.js (runtime) 1.41 kB [entry] [rendered]
chunk {1} main-es2015.0249a613624c51fa5f34.js (main) 209 kB [initial] [rendered]
chunk {2} polyfills-es2015.27661dfa98f6332c27dc.js (polyfills) 36.4 kB [initial] [rendered]
chunk {3} styles.0221bcaaedadb37503af.css (styles) 166 kB [initial] [rendered]

CDN経由で読み込む場合

次に、index.htmlでCDNのcssを取り込むようにします。
npmパッケージはjsDelivrで公開されてるので、探してタグに指定します。

angular.json
            "styles": [
              "src/styles.scss"
            ],
index.html
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/primeicons@2.0.0/primeicons.css" type="text/css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/primeng@8.0.2/resources/primeng.min.css" type="text/css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/primeng@8.0.2/resources/themes/nova-light/theme.css" type="text/css">

ビルドしてみます。

$ ng build --prod --aot

Date: 2019-08-01T23:43:09.112Z
Hash: 9a4ed634ff38e222f351
Time: 15315ms
chunk {0} runtime-es5.741402d1d47331ce975c.js (runtime) 1.41 kB [entry] [rendered]
chunk {1} main-es5.3c79bd2946c435722b96.js (main) 242 kB [initial] [rendered]
chunk {2} polyfills-es5.7f43b971448d2fb49202.js (polyfills) 111 kB [initial] [rendered]

Date: 2019-08-01T23:43:19.891Z
Hash: a80bac2d47e20a33bfdb
Time: 10748ms
chunk {0} runtime-es2015.858f8dd898b75fe86926.js (runtime) 1.41 kB [entry] [rendered]
chunk {1} main-es2015.0249a613624c51fa5f34.js (main) 209 kB [initial] [rendered]
chunk {2} polyfills-es2015.27661dfa98f6332c27dc.js (polyfills) 36.4 kB [initial] [rendered]
chunk {3} styles.09e2c710755c8867a460.css (styles) 0 bytes [initial] [rendered]

styles~.cssのサイズが166kBも変わってきます。

◆注意

インストールするパッケージのバージョンは固定しましょう。
うっかりバージョンアップして、<link>で取得するcssのバージョンと食い違ってしまうと画面崩れが起きる可能性があります。

package.json
  "dependencies": {
    ・・・
    "primeicons": "2.0.0",
    "primeng": "8.0.2",
    ・・・
  },

IVY

IVYとはAngular9で正式リリースされるレンダリングエンジンです。
詳細はリファレンスをご覧下さい。
Angular8ではまだプレビュー版ですが、使うことができます。

tsconfig.ts
{
  "compilerOptions": { ... },
  "angularCompilerOptions": {
    "enableIvy": true
  }
}

IVYなしの場合

$ ng build --prod --aot

Date: 2019-08-01T23:43:09.112Z
Hash: 9a4ed634ff38e222f351
Time: 15315ms
chunk {0} runtime-es5.741402d1d47331ce975c.js (runtime) 1.41 kB [entry] [rendered]
chunk {1} main-es5.3c79bd2946c435722b96.js (main) 242 kB [initial] [rendered]
chunk {2} polyfills-es5.7f43b971448d2fb49202.js (polyfills) 111 kB [initial] [rendered]

Date: 2019-08-01T23:43:19.891Z
Hash: a80bac2d47e20a33bfdb
Time: 10748ms
chunk {0} runtime-es2015.858f8dd898b75fe86926.js (runtime) 1.41 kB [entry] [rendered]
chunk {1} main-es2015.0249a613624c51fa5f34.js (main) 209 kB [initial] [rendered]
chunk {2} polyfills-es2015.27661dfa98f6332c27dc.js (polyfills) 36.4 kB [initial] [rendered]
chunk {3} styles.09e2c710755c8867a460.css (styles) 0 bytes [initial] [rendered]

IVYありの場合

既存プロジェクトの場合、tsconfig.app.jsonにenableIvy:trueを追加するだけです。

tsconfig.app.json
  "angularCompilerOptions": {
    "enableIvy": true
  }

※新規プロジェクトの場合はng new project-name --enable-ivyのように--enable-ivyを指定すればOKです

$ ng build --prod --aot

Compiling @angular/core : module as esm5

Compiling @angular/common : module as esm5

Compiling @angular/platform-browser : module as esm5

Compiling @angular/platform-browser-dynamic : module as esm5

Compiling @angular/router : module as esm5

Date: 2019-08-01T23:53:57.269Z
Hash: a549293d080c890e1895
Time: 53919ms
chunk {0} runtime-es5.741402d1d47331ce975c.js (runtime) 1.41 kB [entry] [rendered]
chunk {1} main-es5.0fc1fd74f3bbc9926e4d.js (main) 231 kB [initial] [rendered]
chunk {2} polyfills-es5.7f43b971448d2fb49202.js (polyfills) 111 kB [initial] [rendered]

Compiling @angular/core : es2015 as esm2015

Compiling @angular/common : es2015 as esm2015

Compiling @angular/platform-browser : es2015 as esm2015

Compiling @angular/platform-browser-dynamic : es2015 as esm2015

Compiling @angular/router : es2015 as esm2015

Date: 2019-08-01T23:54:38.351Z
Hash: 1fa8b85dbae59a28d8b1
Time: 41034ms
chunk {0} runtime-es2015.858f8dd898b75fe86926.js (runtime) 1.41 kB [entry] [rendered]
chunk {1} main-es2015.5e9ea7828c1754ddb647.js (main) 207 kB [initial] [rendered]
chunk {2} polyfills-es2015.27661dfa98f6332c27dc.js (polyfills) 36.4 kB [initial] [rendered]
chunk {3} styles.09e2c710755c8867a460.css (styles) 0 bytes [initial] [rendered]

es2015のmain~.jsで209kBから 207kBに減っています。
今回はテンプレートプロジェクトなので、効果は小さいですが大規模プロジェクトになると効果は大きいんじゃないかと思います。

【番外編】AOTビルド

基本的にはデフォルトでAOTビルド有効なので、気にする必要はないと思いますが一応書いておきます。

以下のように--aotをつけてビルドします。
AOTビルドに関してはリファレンスを見てください。

ng build --aot --prod

--aotなしで本番ビルドした場合のサイズ

ng newしたばかりのもので試してみます。

AOTを無効にする設定をします

angular.json
         ・・・
          "configurations": {
            "production": {
              ・・・
              "aot": false,
              "buildOptimizer": false,
            }
          }
          ・・・
$ ng build --prod

Date: 2019-08-01T23:13:57.002Z
Hash: 7261057f8d9a98bd00a0
Time: 21086ms
chunk {0} runtime-es5.fd090508f518df362df3.js (runtime) 1.42 kB [entry] [rendered]
chunk {1} main-es5.5bf5a74163dfcb858ef4.js (main) 874 kB [initial] [rendered]
chunk {2} polyfills-es5.2aba62e787b3d3e95c6a.js (polyfills) 118 kB [initial] [rendered]

Date: 2019-08-01T23:14:13.404Z
Hash: 1a64fef5e665ce26dc1e
Time: 16346ms
chunk {0} runtime-es2015.6799f3bffa293d78b1fe.js (runtime) 1.42 kB [entry] [rendered]
chunk {1} main-es2015.e69fd787690d8f424782.js (main) 736 kB [initial] [rendered]
chunk {2} polyfills-es2015.3d5c9d6e99ad1936257a.js (polyfills) 60.5 kB [initial] [rendered]
chunk {3} styles.09e2c710755c8867a460.css (styles) 0 bytes [initial] [rendered]

--aotありで本番ビルドした場合のサイズ

$ ng build --prod --aot

Date: 2019-08-01T23:12:24.355Z
Hash: 9a4ed634ff38e222f351
Time: 14796ms
chunk {0} runtime-es5.741402d1d47331ce975c.js (runtime) 1.41 kB [entry] [rendered]
chunk {1} main-es5.3c79bd2946c435722b96.js (main) 242 kB [initial] [rendered]
chunk {2} polyfills-es5.7f43b971448d2fb49202.js (polyfills) 111 kB [initial] [rendered]

Date: 2019-08-01T23:12:35.747Z
Hash: a80bac2d47e20a33bfdb
Time: 11364ms
chunk {0} runtime-es2015.858f8dd898b75fe86926.js (runtime) 1.41 kB [entry] [rendered]
chunk {1} main-es2015.0249a613624c51fa5f34.js (main) 209 kB [initial] [rendered]
chunk {2} polyfills-es2015.27661dfa98f6332c27dc.js (polyfills) 36.4 kB [initial] [rendered]
chunk {3} styles.09e2c710755c8867a460.css (styles) 0 bytes [initial] [rendered]

es2015向けのビルドファイルを見るとmain~.jsが874kBから209kBまで小さくなっています。
ただ、上でも書きましたがAOTビルドはデフォルトで有効になっているので、まったく恩恵にはあずかれませんでした。。。

参考

teracy55
2011年からエンジニアやってます。 最近はAngular/NestJSをメインに Webシステム作ってます。それ以外だと、組み込みエンジニアとしてC/C++でカーナビ開発、Javaで業務システム、PHPでのWebシステム開発、Android/iOSアプリ開発(Flutter、Monaca、CocosCreator)なんかをやってきました。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away