😎

ザ・Angularルネサンス、Angular17!何が変わったのか?

2024/01/18に公開

2023年11月、Angularの新しいバージョン17が公開されました。
バージョン17には色々な新しい特徴が導入され、モーダンウェブ上の開発経験やパフォーマンスを改善できるようになりました。
Angularチームはバージョン17のローンチに対して『Angularルネサンス😎』と言ってましたが、その理由を今から確認しに行きましょう。

リブランディング

本格的な技術の話に入る前に、Angularの新しいロゴの話をしたいと思います。

Angularチームはバージョン17のローンチと共に、 angularの新ロゴ及びブランドを発表しました。
新ロゴはモーダンフレームワークとして進化してきたAngularの未来を示すらしいです。
新しいロゴ素敵ですね!本ロゴの色などもカスタマイズできるようなので、気になる方はこちらのガイダンスページをご参考までに。

新しい公式ドキュメント(angular.dev)



参照:blog.angular.io
リブランディングの一環で、公式ドキュメントも新しい姿に変わりました。UIも改善され、内容もより充実になりました。WebComponentsを採択してブラウザー上の開発環境が体験でき、Playgroundsも新しくできましたので、より気軽にAnguarを触れます。ざっと見てみましたが、angularの文法などが学べるチュートリアルもより探しやすくなった感じです。気になる方はこちらのリンクでご確認ください。新ドキュメントはまだbetaバージョンなので、日本語版はまだサポートされてないです。


Built-in control flow(ビルトイン コントロール フロー)

本題に戻ります。Angular17で一番大きく変わってきたのは新しいテンプレート構文が導入されたことです。
既存の構文は*ngIf*ngSwitch*ngForなどを利用していましたが、多くのAngularユーザーがこの構文に苦労しているところが経緯になったようです。
新構文(built-in control flow)の特徴は以下のようです。

  1. アーゴノミックス(人間工学的)な構文であり、Javascriptに近い形なので直観的だ。(今までより公式ドキュメントを探る頻度が少なくなるかも)
  2. タイプの絞り込み(Type Narrowing)が最適され、タイプチェックが良くなった。
  3. ランタイム中の負担が減り、バンドルサイズも30kbまで減らせるので、コアウェブバイタル(Core Web Vitals)の改善も期待できる。
  4. 別のインポートが必要なわけではなく、このテンプレを使うだけで、自動的に効果がある。
  5. 相当なパフォーマンスの改善

では、細かくどのように変化されたのか、見てみましょう。

条件文

*ngIf@if

以前のバージョン(before)
<div *ngIf="loggedIn; else anonymousUser">
  ユーザーがログインしています。
</div>
<ng-template #anonymousUser>
  ログインしていません。
</ng-template>
バージョン17(after)
@if (loggedIn) {
  ユーザーがログインしています。
} @else {
  ログインしていません。
}

@else if文も使えるようになったようです。 *ngIf文でelse ifのような実装したいならネスティングを使ったり少し長くなったりしますが、ビルトインコントロールフローを使えば簡単に書き換えそうです。形も純粋なjavascriptと似ていますので、より直感的になりましたね。

以前のバージョン(before)
<ng-container *ngIf="a > b; else less">
    {{a}} が {{b}} より大きいです。
</ng-container>

<ng-template #less>
    <ng-container *ngIf="a < b; else equal">
      {{a}} が {{b}} より小さいです。
    </ng-container>
</ng-template>

<ng-template #equal>
    {{a}} と {{b}} は同じです。
</ng-template>
バージョン17(after)
@if (a > b) {
  {{a}} が {{b}} より大きいです。
} @else if (b > a) {
  {{a}} が {{b}} より小さいです。
} @else {
  {{a}} と {{b}} は同じです。
}

*ngSwitch@switch

以前のバージョン(before)
<div [ngSwitch]="accessLevel">
  <admin-dashboard *ngSwitchCase="admin"/>
  <moderator-dashboard *ngSwitchCase="moderator"/>
  <user-dashboard *ngSwitchDefault/>
</div>
バージョン17(after)
@switch (accessLevel) {
  @case ('admin') { <admin-dashboard/> }
  @case ('moderator') { <moderator-dashboard/> }
  @default { <user-dashboard/> }
}

@switchでは*ngSwitchと違ってType Narrowingが大分良くなったらしいです。

ループ及び反復処理

*ngFor@for

以前のバージョン(before)
<ng-container *ngIf="items.length > 0">
  <li *ngFor="let item of items; trackBy: trackByItems">
    Item #{{ idx }}: {{ item.name }}
  </li>
</ng-container>

<li *ngIf="itmes.length === 0">アイテムなし。リストが空いています。</li>
trackByItems(index: number, item: Item): number {
  return item.id;
}
バージョン17(after)
@for (let item of items; track item.id; let idx = $index) {
  <li> Item #{{ idx }}: {{ item.name }} </li>
} @empty {
  <li>アイテムなし。リストが空いています。</li>
}

@emtpyブロックが新しくでき、ループを行うリストが空いているいる時にすぐテンプレなどを設定できるようになりました。今までなら、ngIf文などを合わせて書く必要がありましたが、より簡単に対応できます。
ループ文で良くある性能落ちを改善するため、trackBy機能が勧奨だった反面、Angular17からは性能を確保するため、track機能が必須項目になりました。

このビルトインループ文にて一番大きい変化点と言えば、ランタイムが約90%早くなったということです!

ビルトインfor文とjs-framework-benchmarkのngFor文をパフォーマンス比較した結果。参照:blog.angular.io
上記の表で一番目に立つのはリストの項目を入れ替わったりする時ですね。全1,000行のテーブルから別々の行の内容を入れ替えようとする時、ngForは166.8msがかかりましたが(赤色の箇所)、ビルトインfor文は24.8msしかかかってませんでした。データなど、扱うリストの数が増えれば増えるほど、CPUの負荷が高くなり、ページが遅くなるのではないかと心配している方には朗報です。

マイグレーション

Angular17以前バージョンで作成された旧構文を新構文に書き換えるのはどうするか悩む方、ご心配なさらないでください。angular17では自動マイグレーションできるようにサポートしています。以下のコマンドでマイグレーションができますので、ご参考までに。

ng generate @angular/core:control-flow

Deferrable views(ディファラブル ビュー)

直訳すると、遅延可能なビューですかね。コンポーネントを簡単にレージロード(lazy load)するのができます。今まではモジュールをレージロードしたり、コンポーネント自体を遅延ロードするためには自分でIntersectionObserverロジックを実装したり、ViewContainerRefを活用したり、少し制約がありましたが、Angular17では@deferというすごく簡単な構文でレージロードが対応できるようになりました!👏

残りのサブツリーをレージロードする時のコンポーネントツリー(component tree)。参照:blog.angular.io

@defer (on viewport) {
  <comment-list />
} @placeholder {
  <!-- 上記コンポーネントが呼び出されるうちにこちらのプレースホルダー(placeholder)を見せる-->
  <img src="comments-placeholder.png">
}

on viewportのトリガを使った上記の例を見てみましょう。Angularは最初にplaceholderブラックのコードをレンダーします。その後、ビューポート(view port)の中で該当箇所が見えるようになったら、<comment-list />コンポーネントのロードが始まります。ロードができましたら、 angularはplaceholderを削除し、コンポーネントをレンダーします。
@defer構文はプレースホルダーだけでなく、要素をローディングする時(@loading)、ローディングに失敗し、エラーになった時(@error)にも対応できます。

@defer (on viewport) {
  <comment-list/>
} @loading {
  ローディング中です。。。
} @error {
  ロードが失敗しました😔
} @placeholder(minimum 500ms) {
  <!-- プレースホルダーがちらつかないように最低表示時間500ms設定-->
  <img src="comments-placeholder.png">
}

それ以外にも@defer構文は以下のようなトリガーを提供しています。詳細はこちらの公式ドキュメントからご確認ください。

トリガー 説明
on idle deferブロックのディフォルト設定。ブラウザーがリソース集中タスクに忙しくない時にレージロードする。requestIdleCallback APIを利用して、ブラウザーが一旦idle状態(遊休状態)に入っ他時を認識し、レージロードする。
on immediate ブラウザーをブロックしない範囲でレージロードが自動的に行われる。ちなみに、クライアント側のレンダーリングが終わったら、直ぐ`deferrable view`のエレメントを呼び出す。
on timer(<time>) 設定時間が過ぎた時にレージロードする。単位はmssです。
on viewport もしくは on viewport(<ref>) IntersectionObserver APIを利用してプレースホルダーのエレメントがビューポートに入ったタイミングでレージロードする。
on interaction もしくは on interaction(<ref>) clickkeydownイベントなど、ユーザーインタラクションが行った時にレージロードする。
on hover もしくは on hover(<ref>) mouseenterイベントやfocusinイベントなど、トリガーエリアにマウスホバーされた時、レージロードする。
when <expr> ブール式(boolean)を使用してレージロードの条件を指定できる

Hybrid rendering experience (ハイブリッドレンダリング)

Angular17ではサーバーサイドレンダリング(SSR, server-side rendering)またand静的サイトジェネレーター(SSG, static-site generation)をng newプロムプトでより身近く提供します。また、プロジェクトの生成の時、フラグを付けて使えます。

ng new my-app --ssr

新しいパッケージ:@angular/ssr

SSRパッケージがAngularユニバーサルレポジトリからAngularCLIレポジトリに移動されたそうです。ハイブリッドレンダーリングを実装したいなら、以下のコマンドで@angular/ssrパッケージを設定してください。

ng add @angular/ssr

新Lifecycle hooks (ライフサイクルフックス)

長期的にAngularのSSR、SSGのパフォーマンスを改善するためには直接DOMの操作しないまま、ライフサイクルを通じて要素と相互作用し、サードパーティーライブラリはインスタント化する方が望ましいです。その動きを実験するため、新しいライフサイクルが導入されます。

  • afterRender:アプリケーションがレンダリングを終えた度にライフサイクルが呼び出されます。つまり、change detectionが行われる度に呼び出されます。
  • afterNextRender:アプリケーションがレンダリングを終えた後、一回呼び出されます。AfterViewInitと近い動きですが、SSRでは呼び出されません。
@Component({
  selector: 'my-chart-cmp',
  template: `<div #chart>{{ ... }}</div>`,
})
export class MyChartCmp {
  @ViewChild('chart') chartRef: ElementRef;
  chart: MyChart|null;

  constructor() {
    afterRender(() => {
      const contentHeight = this.contentRef.nativeElement.scrollHeight;
    }, {phase: AfterRenderPhase.Read});
    afterNextRender(() => {
      this.chart = new MyChart(this.chartRef.nativeElement);
    }, {phase: AfterRenderPhase.Write});
  }
}

それぞれのフックはフェーズ値(phase)をサポートしており、Angularの中でコールバックをスケジュールします。これはレイアウトの乱れによる再計算など要らない作業を減少させ、パフォーマンスを向上が期待できます。

新ビルドシステム:Viteesbuild

Viteesbuildのような新しいビルドシステムが完全サポートされるようになりました。
Angular17をプレビューした開発者によると、viteesbuild共に活用した時、ビルド時間を67%節約したそうです。それに加え、SSR(サーバーサイドレンダリング)及びSSG(スタティックサイトジェネレーション)を使用すると、ng buildで約87%の速度向上され、ng serveの編集リフレッシュループも約80%高速化されたそうです。後程、既存のプロジェクトをハイブリッドレンダーリングにマイグレーションしたい方はこちらのガイドをご覧ください。

AngularのDevToolsに新機能:依存性注入のデバッグ

AngularのDevToolsはAngularアプリケーションがデバッグできるブラウザの拡張機能です。この開発ツールを利用すると、コンポーネントの構造やプロファイリングなどが確認できます。今回のAngular17の公開と共にAngularのDevToolsの新機能も公開されました。
Angularフレームワークのランタイムに連結することで、インジェクターツリー(injector tree)を調べられ、コンポーネント間の依存性に対して確認ができるようになりました。DevToolsでは以下の項目をUIとしてプレビューできます。

  • コンポーネントインスペクター(component inspector)からコンポーネント間の依存性確認
  • インジェクターツリー(Injector tree)、依存関係のパス
  • それぞれのインジェクターから宣言されたプロバイダー

    コンポーネントの依存性またその構造が確認できます。参照:blog.angular.io
    DevToolsをもっと詳しく知りたい方ならこちらの公式ドキュメントからご覧ください。実際に設置して確認したい方はChrome ウェブストアもしくはFirefox アドオンから見つけられますので、ご参考までに。

Standalone(スタンドアロン)APIの支援

スタンドアロンコンポーネントはAngular アプリケーションを構築するための簡略化された方法を提供します。全てのng generateコマンドはスタンドアロンコンポーネント及びディレクティブ、パイプをスキャフォールド(scaffold)できるようになります。
以下のコマンドで既存のプロジェクトをスタンドアロン形式にマイグレーションができます。スタンドアロンに対してもっと知りたい方ならこちらの公式ドキュメントをご参考ください。

ng generate @angular/core:standalone

View Transitions(ビュートランジション)のサポート

View Transition APIは異なる DOM 状態間のアニメーション遷移を簡単に作成する仕組みを提供し、同時に DOM コンテンツも単一の手順で更新できるAPIです。実験的な機能であり、Chrome,Edgeのブラウザーのみサポートされていますが、こちらの機能がAngular17からwithViewTransitionsというオプションで使えるようになったとのことです。

bootstrapApplication(App, {
  providers: [
    provideRouter(routes, withViewTransitions()),
  ]
});

イメージディレクティブから自動preconnectリンクの生成

AngularイメージディレクティブであるNgOptimizedImageが自動的にpreconnectリンクを生成します。もしディレクティブがLCPイメージに対して、自動的にオリジンを識別できず、preconnectリンクが見つけられないと、開発中にワーニングを表示するそうです。

<img ngSrc="cat.jpg" width="400" height="200" priority>

NgOptimiedImageディレクティブはイメージロードに最適化するためのディレクティブで、上記のようにsrcの代わりにngSrcの属性を利用したりしています。今回のアップデートにより、いちいちpreconnectリンクの設定をする必要がなくなり、作業がしやすくなった感じですね。
NgOptimizedImageに関して詳細が気になる方はこちらでご覧ください。

アニメーションモージュルのレージロード

@angular/animations, @angular/platform-browserなど、Angularでアニメーションのために使われるモージュルをレージロードできるようになりました。provideAnimationsAsyncというプロバイダーを利用して、最初のバンドルサイズを約60kb(16kb gzipped)節約できます。

import { provideAnimationsAsync } from '@angular/platform-browser/animations-async';

bootstrapApplication(RootCmp, {
  providers: [provideAnimationsAsync()]
});

Input value transforms(インプットバリュートランスフォーム)

Inputの型変換がしやすくなりました。

@Component({
  standalone: true,
  selector: 'my-expander',
  template: ``
})
export class Expander {
  @Input() expanded: boolean = false;
}
<!-- エラー:string is not assignable to boolean -->
<my-expander expanded/> 

<!-- 正しいプロパティバインディングの方法はこちら-->
<my-expander [expanded]="true"/>

上記の例で括弧をつけてないプロパティになると、stringを受け取る形になるため、型のエラーが出てしまいします。Angular17ではこのような問題は簡単に解決し、よりシンプリにプロパティバインディングできるようになりました。

import { booleanAttribute } from '@angular/core';

@Component({
  standalone: true,
  selector: 'my-expander',
  template: ``
})

export class Expander {
  @Input({ transform: booleanAttribute }) expanded: boolean = false;
}

boolean型に扱いたいならbooleanAttributeを、number型ならnumberAttribute関数を使えば、対応できます。

styles, styleUrls:string型(文字列型)も支援

Angularでは一つのコンポーネントに複数のスタイルが適用適用できます。なので、今までは一つのスタイルを適用してもstylesstyleUrlsを配列として扱うしかなかったんですが、Angular17からはそうする必要がなくなりました。インラインスタイルで書いてもオッケーで、一つのスタイルシートを適用するであれば、文字列(string)タイプとして書いても構いません。

以前のバージョン(before)
@Component({
  styles: [`...インラインスタイル...`]
  // もしくは
  styleUrls: ['styles.css']
})
バージョン17(after)
@Component({
  styles: `...インラインスタイル...`
  // もしくは
    styleUrl: 'styles.css'
})

次は?

最近、導入されたAngularのSignal機能のプレビューが終わり、今はeffect関数に対して開発者のプレビュー作業を行なっているとのことです。
次のAngular18まではこの作業をまとめ、Signalに基づいたinput、 view queries など、signal周りの機能が導入したいということです。
Signalsだけでなく、テストやAngular Material3も作業を続いているようなので、お楽しみに。

最後に

厳密に言えば、記事の中でAngular16から導入されたものも紹介されていましたが、私もこの度、新しく知ってきたこともありました。(Angularの世界、広いな〜)
Angular17の特徴で一番目に立つのはやはりBuilt-in control flowDeferrable Viewの導入でパフォーマンスの向上が期待されるところですね。本当にAngularルネサンスと言えるほど、色々変わってきたと思います。Angularの構文が特殊なところからAngular学習のハードルが高いという話もあったようで、これをきっかけにもっと多くの人がAngularに興味を持って、使っていただければ嬉しいです。

脚注
  1. 参考資料:
    https://blog.angular.io/introducing-angular-v17-4d7033312e4b
    https://blog.angular.io/announcing-angular-dev-1e1205fa3039
    https://angular.io/guide/control_flow
    https://angular.io/guide/defer
    https://angular.jp/guide/devtools
    https://angular.jp/guide/standalone-components
    https://angular.io/guide/inputs-outputs#configuring-the-child-component-input-with-transform-functions ↩︎

Discussion