らいふうっどの閑話休題

興味のあることをゆる~く書いていく

ポートフォリオサイトを Angular: Standalone Components 化してみる

変更前
ディレクトリ構成

~/work/portfolio
├── README.md
├── angular.json
├── deploy.sh
├── e2e
│   ├── protractor.conf.js
│   ├── src
│   │   ├── app.e2e-spec.ts
│   │   └── app.po.ts
│   └── tsconfig.e2e.json
├── firebase.json
├── icon_512.png
├── install.sh
├── package-lock.json
├── package.json
├── renovate.json
├── sass-lint.yml
├── src
│   ├── app
│   │   ├── app-routing.module.ts
│   │   ├── app.component.html
│   │   ├── app.component.scss
│   │   ├── app.component.spec.ts
│   │   ├── app.component.ts
│   │   ├── app.module.ts
│   │   ├── component
│   │   │   ├── card
│   │   │   │   ├── card.component.html
│   │   │   │   ├── card.component.scss
│   │   │   │   ├── card.component.spec.ts
│   │   │   │   ├── card.component.ts
│   │   │   │   ├── card.module.ts
│   │   │   │   └── card.ts
│   │   │   ├── footer
│   │   │   │   ├── footer.component.html
│   │   │   │   ├── footer.component.scss
│   │   │   │   ├── footer.component.spec.ts
│   │   │   │   ├── footer.component.ts
│   │   │   │   └── footer.module.ts
│   │   │   └── header
│   │   │       ├── header.component.html
│   │   │       ├── header.component.scss
│   │   │       ├── header.component.spec.ts
│   │   │       ├── header.component.ts
│   │   │       └── header.module.ts
│   │   ├── pages
│   │   │   ├── about
│   │   │   │   ├── about-routing.module.ts
│   │   │   │   ├── about.component.html
│   │   │   │   ├── about.component.scss
│   │   │   │   ├── about.component.spec.ts
│   │   │   │   ├── about.component.ts
│   │   │   │   ├── about.module.ts
│   │   │   │   ├── about.service.spec.ts
│   │   │   │   └── about.service.ts
│   │   │   ├── community
│   │   │   │   ├── community-routing.module.ts
│   │   │   │   ├── community.component.html
│   │   │   │   ├── community.component.scss
│   │   │   │   ├── community.component.spec.ts
│   │   │   │   ├── community.component.ts
│   │   │   │   └── community.module.ts
│   │   │   ├── home
│   │   │   │   ├── home-routing.module.ts
│   │   │   │   ├── home.component.html
│   │   │   │   ├── home.component.scss
│   │   │   │   ├── home.component.spec.ts
│   │   │   │   ├── home.component.ts
│   │   │   │   └── home.module.ts
│   │   │   └── page-not-found
│   │   │       ├── page-not-found-routing.module.ts
│   │   │       ├── page-not-found.component.html
│   │   │       ├── page-not-found.component.scss
│   │   │       ├── page-not-found.component.spec.ts
│   │   │       ├── page-not-found.component.ts
│   │   │       └── page-not-found.module.ts
│   │   └── shared
│   │       └── service
│   │           ├── card.service.spec.ts
│   │           └── card.service.ts
│   ├── assets
│   │   ├── i18n
│   │   │   ├── en-us.json
│   │   │   └── ja.json
│   │   ├── imgaes
│   │   │   ├── chirimen.png
│   │   │   ├── conference.png
│   │   │   ├── docswell.png
│   │   │   ├── edit.png
│   │   │   ├── facebook.png
│   │   │   ├── github-logo.png
│   │   │   ├── hatenablog-logo.svg
│   │   │   ├── icon_128.png
│   │   │   ├── mdn.png
│   │   │   ├── ng-gunma-logo-plan2.png
│   │   │   ├── ng-japan2_full.png
│   │   │   ├── slideshare.png
│   │   │   └── twitter.png
│   │   └── json
│   │       ├── card.json
│   │       └── community.json
│   ├── environments
│   │   ├── environment.prod.ts
│   │   └── environment.ts
│   ├── favicon.ico
│   ├── index.html
│   ├── karma.conf.js
│   ├── main.ts
│   ├── polyfills.ts
│   ├── styles.scss
│   ├── test.ts
│   ├── tsconfig.app.json
│   ├── tsconfig.spec.json
│   └── tslint.json
├── tips.md
├── tree.txt
├── tsconfig.json
└── tslint.json

21 directories, 97 files

pagespeedでのスコア確認

webpack analyzer でのスコア確認

■ 圧縮なし

  • All (2.77 MB)
  • common.983dfccb34602eca.js (4.34 KB)
  • main.182c9c5090b94872.js (2.64 MB)
  • polyfills.66c4a90d2e8bcb8c.js (106.28 KB)
  • runtime.457c635ab68bf03a.js(2.82 KB)
  • 105.4d94d273aa012b92.js(6.81 KB)
  • 561.131088f5a5389c24.js (3.29 KB)
  • 698.5cdd0d659f30baa9.js (3.06 KB)
  • 784.b57f8729eea41541.js (2.2 KB)

■ 圧縮あり

  • All (143.03 KB)
  • common.983dfccb34602eca.js (1.11 KB)
  • main.182c9c5090b94872.js (125.45 KB)
  • polyfills.66c4a90d2e8bcb8c.js (11.69 KB)
  • runtime.457c635ab68bf03a.js (1.53 KB)
  • 105.4d94d273aa012b92.js(1.4 KB)
  • 561.131088f5a5389c24.js (723 B)
  • 698.5cdd0d659f30baa9.js (703 B)
  • 784.b57f8729eea41541.js (478 B)

ビルド時間の確認

$ npm run build

> portfolio@0.0.0 build
> ng build --configuration production

✔ Browser application bundle generation complete.
✔ Copying assets complete.
✔ Index html generation complete.

Initial Chunk Files           | Names                                      |  Raw Size | Estimated Transfer Size
main.182c9c5090b94872.js      | main                                       | 425.18 kB |               110.18 kB
styles.28eec00e2cd6ca69.css   | styles                                     | 126.62 kB |                12.07 kB
polyfills.66c4a90d2e8bcb8c.js | polyfills                                  |  33.02 kB |                10.63 kB
runtime.457c635ab68bf03a.js   | runtime                                    |   2.82 kB |                 1.36 kB

                              | Initial Total                              | 587.64 kB |               134.23 kB

Lazy Chunk Files              | Names                                      |  Raw Size | Estimated Transfer Size
105.4d94d273aa012b92.js       | pages-about-about-module                   |   3.33 kB |                 1.21 kB
common.983dfccb34602eca.js    | common                                     |   2.29 kB |              1003 bytes
561.131088f5a5389c24.js       | pages-community-community-module           |   1.29 kB |               630 bytes
698.5cdd0d659f30baa9.js       | pages-home-home-module                     |   1.24 kB |               620 bytes
784.b57f8729eea41541.js       | pages-page-not-found-page-not-found-module | 865 bytes |               415 bytes

Build at: 2023-05-13T07:33:33.584Z - Hash: 044de2ded241b95a - Time: 2461ms
$

Standalone Components マイグレーション実行
Standalone Components マイグレーションコマンドを実行

■ 参考記事

Angular アプリケーションを standalone にマイグレーションする - とんかつ時々あんどーなつ

■ ①Convert all components, directives and pipes to standalone

# マイグレーションコマンド
$ npx ng generate @angular/core:standalone
# マイグレーションモードの選択:Convert all components, directives and pipes to standalone
? Choose the type of migration: Convert all components, directives and pipes to standalone
? Which path in your project should be migrated? ./
    🎉 Automated migration step has finished! 🎉
    IMPORTANT! Please verify manually that your application builds and behaves as expected.
    See https://angular.io/guide/standalone-migration for more information.
UPDATE src/app/component/card/card.component.ts (498 bytes)
UPDATE src/app/pages/home/home.component.ts (724 bytes)
UPDATE src/app/pages/about/about.component.ts (368 bytes)
UPDATE src/app/pages/community/community.component.ts (749 bytes)
UPDATE src/app/pages/page-not-found/page-not-found.component.ts (327 bytes)
UPDATE src/app/component/header/header.component.ts (507 bytes)
UPDATE src/app/component/footer/footer.component.ts (297 bytes)
UPDATE src/app/component/card/card.module.ts (320 bytes)
UPDATE src/app/pages/home/home.module.ts (467 bytes)
UPDATE src/app/pages/about/about.module.ts (391 bytes)
UPDATE src/app/pages/community/community.module.ts (502 bytes)
UPDATE src/app/pages/page-not-found/page-not-found.module.ts (364 bytes)
UPDATE src/app/component/header/header.module.ts (520 bytes)
UPDATE src/app/component/footer/footer.module.ts (269 bytes)
UPDATE src/app/app.component.spec.ts (588 bytes)
UPDATE src/app/component/card/card.component.spec.ts (601 bytes)
UPDATE src/app/component/footer/footer.component.spec.ts (615 bytes)
UPDATE src/app/component/header/header.component.spec.ts (615 bytes)
UPDATE src/app/pages/about/about.component.spec.ts (608 bytes)
UPDATE src/app/pages/community/community.component.spec.ts (636 bytes)
UPDATE src/app/pages/home/home.component.spec.ts (601 bytes)
UPDATE src/app/pages/page-not-found/page-not-found.component.spec.ts (659 bytes)
$


■ ②Remove unnecessary NgModule classes

# マイグレーションコマンド
$ npx ng generate @angular/core:standalone 
# マイグレーションモードの選択:Remove unnecessary NgModule classes
? Choose the type of migration: Remove unnecessary NgModule classes
? Which path in your project should be migrated? ./
    🎉 Automated migration step has finished! 🎉
    IMPORTANT! Please verify manually that your application builds and behaves as expected.
    See https://angular.io/guide/standalone-migration for more information.
DELETE src/app/component/card/card.module.ts
DELETE src/app/component/header/header.module.ts
DELETE src/app/component/footer/footer.module.ts
UPDATE src/app/pages/home/home.module.ts (398 bytes)
UPDATE src/app/pages/community/community.module.ts (433 bytes)
UPDATE src/app/app.module.ts (645 bytes)
$ 


■ ③Bootstrap the application using standalone APIs

# マイグレーションコマンド
$ npx ng generate @angular/core:standalone
# マイグレーションモードの選択:Bootstrap the application using standalone APIs
? Choose the type of migration: Bootstrap the application using standalone APIs
? Which path in your project should be migrated? ./
    🎉 Automated migration step has finished! 🎉
    IMPORTANT! Please verify manually that your application builds and behaves as expected.
    See https://angular.io/guide/standalone-migration for more information.
DELETE src/app/app.module.ts
UPDATE src/main.ts (919 bytes)
UPDATE src/app/app.component.ts (318 bytes)
UPDATE src/app/app.component.spec.ts (576 bytes)
$ 


■ ④ビルドして、エラー箇所確認

$ npm run build

> portfolio@0.0.0 build
> ng build --configuration production

✔ Browser application bundle generation complete.

Error: src/app/app.component.html:1:1 - error NG8001: 'lw-header' is not a known element:
1. If 'lw-header' is an Angular component, then verify that it is included in the '@Component.imports' of this component.
2. If 'lw-header' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@Component.schemas' of this component to suppress this message.

1 <lw-header></lw-header>
  ~~~~~~~~~~~

  src/app/app.component.ts:6:18
    6     templateUrl: './app.component.html',
                       ~~~~~~~~~~~~~~~~~~~~~~
    Error occurs in the template of component AppComponent.


Error: src/app/app.component.html:3:1 - error NG8001: 'lw-footer' is not a known element:
1. If 'lw-footer' is an Angular component, then verify that it is included in the '@Component.imports' of this component.
2. If 'lw-footer' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@Component.schemas' of this component to suppress this message.

3 <lw-footer></lw-footer>
  ~~~~~~~~~~~

  src/app/app.component.ts:6:18
    6     templateUrl: './app.component.html',
                       ~~~~~~~~~~~~~~~~~~~~~~
    Error occurs in the template of component AppComponent.
$

HeaderComponent, FooterComponent を AppComponent にインポートして修正完了


■ ⑤エラー箇所を修正後、再ビルド

$ npm run build

> portfolio@0.0.0 build
> ng build --configuration production

✔ Browser application bundle generation complete.
✔ Copying assets complete.
✔ Index html generation complete.

Initial Chunk Files           | Names                                      |  Raw Size | Estimated Transfer Size
main.cf3355b77227c992.js      | main                                       | 422.79 kB |               109.77 kB
styles.28eec00e2cd6ca69.css   | styles                                     | 126.62 kB |                12.07 kB
polyfills.66c4a90d2e8bcb8c.js | polyfills                                  |  33.02 kB |                10.63 kB
runtime.c44cd13664e05a20.js   | runtime                                    |   2.82 kB |                 1.35 kB

                              | Initial Total                              | 585.24 kB |               133.82 kB

Lazy Chunk Files              | Names                                      |  Raw Size | Estimated Transfer Size
105.163cd318093059b1.js       | pages-about-about-module                   |   3.36 kB |                 1.21 kB
common.f2d02f200b7b8184.js    | common                                     |   2.10 kB |               945 bytes
561.5cac99a41495d522.js       | pages-community-community-module           |   1.30 kB |               649 bytes
698.d7ae0864dbc82789.js       | pages-home-home-module                     |   1.25 kB |               628 bytes
784.57c81b8e75c4007d.js       | pages-page-not-found-page-not-found-module | 896 bytes |               430 bytes

Build at: 2023-05-13T09:15:04.219Z - Hash: a1f6ba84e01aeaf5 - Time: 2449ms
$ 


■ ⑥ローカルで挙動確認

変更後
ディレクトリ構成

~/work/portfolio
├── README.md
├── angular.json
├── deploy.sh
├── e2e
│   ├── protractor.conf.js
│   ├── src
│   │   ├── app.e2e-spec.ts
│   │   └── app.po.ts
│   └── tsconfig.e2e.json
├── firebase.json
├── icon_512.png
├── install.sh
├── package-lock.json
├── package.json
├── renovate.json
├── sass-lint.yml
├── src
│   ├── app
│   │   ├── app-routing.module.ts # 要手動修正
│   │   ├── app.component.html
│   │   ├── app.component.scss
│   │   ├── app.component.spec.ts
│   │   ├── app.component.ts
│   │   ├── component
│   │   │   ├── card
│   │   │   │   ├── card.component.html
│   │   │   │   ├── card.component.scss
│   │   │   │   ├── card.component.spec.ts
│   │   │   │   ├── card.component.ts
│   │   │   │   └── card.ts
│   │   │   ├── footer
│   │   │   │   ├── footer.component.html
│   │   │   │   ├── footer.component.scss
│   │   │   │   ├── footer.component.spec.ts
│   │   │   │   └── footer.component.ts
│   │   │   └── header
│   │   │       ├── header.component.html
│   │   │       ├── header.component.scss
│   │   │       ├── header.component.spec.ts
│   │   │       └── header.component.ts
│   │   ├── pages
│   │   │   ├── about
│   │   │   │   ├── about-routing.module.ts # 要手動修正
│   │   │   │   ├── about.component.html
│   │   │   │   ├── about.component.scss
│   │   │   │   ├── about.component.spec.ts
│   │   │   │   ├── about.component.ts
│   │   │   │   ├── about.module.ts # 要手動修正
│   │   │   │   ├── about.service.spec.ts
│   │   │   │   └── about.service.ts
│   │   │   ├── community
│   │   │   │   ├── community-routing.module.ts # 要手動修正
│   │   │   │   ├── community.component.html
│   │   │   │   ├── community.component.scss
│   │   │   │   ├── community.component.spec.ts
│   │   │   │   ├── community.component.ts
│   │   │   │   └── community.module.ts # 要手動修正
│   │   │   ├── home
│   │   │   │   ├── home-routing.module.ts # 要手動修正
│   │   │   │   ├── home.component.html
│   │   │   │   ├── home.component.scss
│   │   │   │   ├── home.component.spec.ts
│   │   │   │   ├── home.component.ts
│   │   │   │   └── home.module.ts # 要手動修正
│   │   │   └── page-not-found
│   │   │       ├── page-not-found-routing.module.ts # 要手動修正
│   │   │       ├── page-not-found.component.html
│   │   │       ├── page-not-found.component.scss
│   │   │       ├── page-not-found.component.spec.ts
│   │   │       ├── page-not-found.component.ts
│   │   │       └── page-not-found.module.ts # 要手動修正
│   │   └── shared
│   │       └── service
│   │           ├── card.service.spec.ts
│   │           └── card.service.ts
│   ├── assets
│   │   ├── i18n
│   │   │   ├── en-us.json
│   │   │   └── ja.json
│   │   ├── imgaes
│   │   │   ├── chirimen.png
│   │   │   ├── conference.png
│   │   │   ├── docswell.png
│   │   │   ├── edit.png
│   │   │   ├── facebook.png
│   │   │   ├── github-logo.png
│   │   │   ├── hatenablog-logo.svg
│   │   │   ├── icon_128.png
│   │   │   ├── mdn.png
│   │   │   ├── ng-gunma-logo-plan2.png
│   │   │   ├── ng-japan2_full.png
│   │   │   ├── slideshare.png
│   │   │   └── twitter.png
│   │   └── json
│   │       ├── card.json
│   │       └── community.json
│   ├── environments
│   │   ├── environment.prod.ts
│   │   └── environment.ts
│   ├── favicon.ico
│   ├── index.html
│   ├── karma.conf.js
│   ├── main.ts
│   ├── polyfills.ts
│   ├── styles.scss
│   ├── test.ts
│   ├── tsconfig.app.json
│   ├── tsconfig.spec.json
│   └── tslint.json
├── tips.md
├── tree.txt
├── tsconfig.json
└── tslint.json

14969 directories, 140600 files

pagespeedでのスコア確認

pagespeed.web.dev

webpack analyzer でのスコア確認

■ 圧縮なし

  • All (2.76 MB)
  • common.f2d02f200b7b8184.js (3.89 KB)
  • main.cf3355b77227c992.js (2.64 MB)
  • polyfills.66c4a90d2e8bcb8c.js (106.28 KB)
  • runtime.c44cd13664e05a20.js(2.82 KB)
  • 105.163cd318093059b1.js (6.88 KB)
  • 561.5cac99a41495d522.js (3.3 KB)
  • 698.d7ae0864dbc82789.js (3.06 KB)
  • 784.57c81b8e75c4007d.js (2.26 KB)

■ 圧縮あり

  • All (142.56 KB)
  • common.f2d02f200b7b8184.js (1.05 KB)
  • main.cf3355b77227c992.js (124.96 KB)
  • polyfills.66c4a90d2e8bcb8c.js (11.69 KB)
  • runtime.c44cd13664e05a20.js(1.53 KB)
  • 105.163cd318093059b1.js (1.43 KB)
  • 561.5cac99a41495d522.js (740 B)
  • 698.d7ae0864dbc82789.js (716 B)
  • 784.57c81b8e75c4007d.js (499 B)

ビルド時間の確認

$ npm run build

> portfolio@0.0.0 build
> ng build --configuration production

✔ Browser application bundle generation complete.
✔ Copying assets complete.
✔ Index html generation complete.

Initial Chunk Files           | Names                                      |  Raw Size | Estimated Transfer Size
main.cf3355b77227c992.js      | main                                       | 422.79 kB |               109.77 kB
styles.28eec00e2cd6ca69.css   | styles                                     | 126.62 kB |                12.07 kB
polyfills.66c4a90d2e8bcb8c.js | polyfills                                  |  33.02 kB |                10.63 kB
runtime.c44cd13664e05a20.js   | runtime                                    |   2.82 kB |                 1.35 kB

                              | Initial Total                              | 585.24 kB |               133.82 kB

Lazy Chunk Files              | Names                                      |  Raw Size | Estimated Transfer Size
105.163cd318093059b1.js       | pages-about-about-module                   |   3.36 kB |                 1.21 kB
common.f2d02f200b7b8184.js    | common                                     |   2.10 kB |               945 bytes
561.5cac99a41495d522.js       | pages-community-community-module           |   1.30 kB |               649 bytes
698.d7ae0864dbc82789.js       | pages-home-home-module                     |   1.25 kB |               628 bytes
784.57c81b8e75c4007d.js       | pages-page-not-found-page-not-found-module | 896 bytes |               430 bytes

Build at: 2023-05-13T09:15:04.219Z - Hash: a1f6ba84e01aeaf5 - Time: 2449ms
$ 

比較

■ 圧縮なし

ファイル名 変更前 変更後 差分 単位
All 2.77 2.76 0.01 MB
common.*****.js 4.34 3.89 0.45 KB
main.******.js 2.64 2.64 0 MB
polyfills.*****.js 106.28 106.28 0 KB
runtime.****.js 2.82 2.82 0 KB
105.*****.js 6.81 6.88 -0.07 KB
561.*****.js 3.29 3.3 -0.01 KB
698.*****.js 3.06 3.06 0 KB
784.*****.js 2.2 2.26 -0.06 KB

■ 圧縮あり

ファイル名 変更前 変更後 差分 単位
All 143.03 142.56 0.47 KB
common.*****.js 1.11 1.05 0.06 KB
main.******.js 125.45 124.96 0.49 KB
polyfills.*****.js 11.69 11.69 0 KB
runtime.****.js 1.53 1.53 0 KB
105.*****.js 1.4 1.43 -0.03 KB
561.*****.js 723 740 -17 B
698.*****.js 703 716 -13 B
784.*****.js 478 499 -21 B

■ ビルド時間

変更前(ms) 変更後(ms) 差(ms)
2461 2449 12

まとめ
  • 小規模アプリケーションなら、Standalone Components へのマイグレーションなら比較的容易です。
  • 小規模なので、パフォーマンスで劇的な差を確認できませんでした。
  • バンドルサイズも誤差の範疇でした。
  • ビルド時間はごく僅かですが、早くなりました。
  • xxxxx.module.ts や xxxxx-routing.module.ts は、個別に修正する必要があります。
  • ブランチを作成して、早めに Standalone Components へのマイグレーションをしてみる事をお勧めします。
  • ただ闇雲に実施するのではなく、計画や範囲を決めて着手すると良いと思います。
謝辞

@kasaharu さんの下記記事のおかげで、困ることなくマイグレーション出来ました。

有難うございました。

kasaharu.hatenablog.com