本セッションの登壇者
セッション動画
では、「SSR中心で考えるAngularアプリケーション開発」というタイトルで発表させていただきます。
まず最初に自己紹介ですが、lalcolacoといいます。Google認定のエキスパートとして活動していて、Angularの日本ユーザー会代表もやっています。Classiという会社で働いており、また、最近はポッドキャストもやっています。
コミュニティの紹介ですが、Angular日本ユーザー会のXとYouTubeもやっております。Discordを拠点としているので、コミュニティのサイトから是非気軽に参加してください。
今回、アジェンダは2つです。まずはAngularのSSRの最近のアップデートをおさらいした上で、後半はそのSSR中心で考えるこれからのAngularアプリケーションの開発についてお話しします。
AngularがSSRの苦手を克服!!
まず、AngularのSSRアップデートおさらいです。この2年ぐらいで、AngularのSSR周りの環境が大きく変わってきています。昔はAngularはSSRが特に苦手と言われていましたが、最近はそうではなくなってきて、いわゆる第1級サポートに近い感じになってきています。
今回紹介するのがざっくり7つありまして、それぞれ説明していきます。
まず1つ目に、「ng new」に「--ssr」というフラグが追加されるようになりました。Angular SSRを使って新規プロジェクトを作成する際に「SSRを有効にしますか?」という質問に「Yes」と応答するだけで、必要なファイルやコードが全部最初から生成されてSSR可能な状態になります。
ローカルで開発する時も、ローカルサーバーでSSRされた状態で開発、デプロイができるということで、SSRの自動セットアップがこの1年くらいでできるようになってきています。
生成されるコードの中で使われているのは @angular/ssr
というパッケージです。AngularのアプリケーションをNode.js上で実行するためのユーティリティで、Node.jsであれば、どのようにサーバー構築しているかは問いません。画面右側のコード例だとExpressで立てているサーバーを対象にしていますが、LambdaやCloud Functionsといったいろいろなサーバー環境で使いやすい環境になっています。以前はそれぞれフレームワークごとにアダプターみたいなものがあったのが、今はこのNode.js向け1個になっています。
SSGの方にも力を入れていて、プリレンダリングが可能になっています。ビルド時に最初のファーストビューの状態をプリレンダリングしておくことによって、ファーストビューで何も表示されないということがなくなります。これも1コマンドで既存のアプリに追加できますし、最初のフラグでSSRを有効にしていれば最初から有効になっています。SSRは導入にサーバーが必要など、乗り越えるハードルがあるんですが、このプリレンダリングには特に追加のインフラも必要なく、新しく導入しやすい要素になっていると思います。これに対応できるようにアプリを変えていけば、だんだんとSSRもしやすくなっていくと思われます。
レンダリングの無駄をなくしてパフォーマンスが向上
プリレンダリングやSSRに合わせてパフォーマンスを上げていくためには、レンダリングの無駄をなくす必要があります。以前はSSRされた状態のHTMLが配信されて、その上で改めてクライアントサイドでAngularを起動する時に、元々あったHTMLを破壊してしまうという問題があったんですが、これは去年のバージョン17のアップデートで、元のDOMを再利用してアプリケーションを起動するように変わっています。これによって、操作途中のものが後でリセットされるみたいなことがなくなりましたし、ファーストビューがちらつかないなどユーザー体験が改善されました。こちらも追加するコードはほぼ1行で、開発者側でがんばらなくていいようにデザインされていますね。
このように、クライアントハイドレーションを有効にすれば導入できます。
他にもパフォーマンスが改善されていて、HTTPレスポンスの再利用も自動的に行われます。
Angularが提供するHTTPクライアントを使っている場合、SSR中に発生したリクエストとレスポンスがキャッシュされ、レンダリングされたHTMLに埋め込まれます。それを配信した時に、最初のファーストビューのためのクライアントサイドのレンダリングでは、そのキャッシュされたレスポンスを再利用するので同じリクエストを送らなくていい。その分、HTTPリクエストが1つ減るのでパフォーマンスが改善されますし、ちらつきもなくなります。時間差でデータが変わっても許容できる場合はこのキャッシュを使うことで、サーバーで取得したデータをもう1回使えます。有効/無効を切り替えられますので、常に最新のリアルタイムのものが必要な時はキャッシュを使わないようにもできます。
また、HTTPクライアントに関連して、AngularのHTTPクライアントはデフォルトではまだXHRを裏側で使いますが、Fetch APIを使うように変えるモードも提供されています。現状オプトインですが、じきにオプトアウトになるでしょう。
このアップデートの利点は、Node.jsではない環境、たとえばCloudflare WorkesなどではXHRが用意されていませんが、Fetch
しかない環境でもSSRでき、実行環境が大きく広がることです。
ハイドレーション後にSSRが配信されて、それがクライアントファイルにレンダリングされた後のちょっとした時間の間に溜まったDOMのイベントは基本的に捨てられますが、これを記録しておいて、ハイドレーションが終わった後に流し直すことでユーザーの操作が捨てられないようにする「イベントリプレイ」という仕組みも入っています。
これも1行追加するだけです。
また、ページ全体ではなく、ユーザーが表示したり、触ったり、何かしら必要になった時までそのコンポーネントのハイドレーションを遅延させる仕組みも、次のバージョンで入ってきそうです。
こちらも、テンプレート上で囲むだけで実現できる予定です。
アプリケーションをSSR前提で考える
これらを前提にした上で、アプリケーションをどうすればいいのかと考えると、大きく3つあります。1つはSSRを可能にすること、次に、より効果的にするためにハイドレーションを可能にすること、最後がこれから来る部分的ハイドレーションに備えた設計にしておくということです。
SSRが不可能になる理由は、ブラウザのAPIやDOMのAPIに強く依存したりすることです。DOMは基本的に問題ありませんが、それでも最新の機能やDOMのたとえばCSSによって計算された後のサイズは、どうしてもサーバーサイドには取得不可能なので、ブラウザだけでしか実行できなくなってしまいます。
したがって、Node.jsと互換性のあるAPIを選ぶ、Platform IDごとに条件分岐する、Angularでブラウザだけ実行されるライフサイクルを利用するなど、何らかの対策をしていく必要があります。
こういったことを意識していく必要があります。
ハイドレーションを可能にする設計へ
次に、ハイドレーションを可能にするというのはどういうことかというと、生成されたDOMに対してそれを再利用するには、同じ状態のDOMを再構成できる必要があります。つまりDOMレンダリング結果がレンダリングの都度異なっていて非決定的な場合は再利用できません。同じ入力を与えると同じ結果が得られるレンダリングでなければ、何と何を対応させていいかわからなくなるので、レンダリング結果が決定性を持つことが必要です。何ができなくなるかというと、レンダリングした後にDOMをいじるとか、あるいはタイミング依存のデータを出すことなどです。
対策としては、まずはレンダリング後のDOM操作をやめる、またはブラウザでだけ行うようにすることです。サーバーサイドでは、DOM操作を行う前の状態をレンダリングしておいて、クライアントサイドへその操作を遅延させることが必要になります。
実際のコードはこのようになります。または、無理にハイドレーションをせず、壊れる前にやめておくために ngSkipHydration
を行うことがありますが、そうするとハイドレーションができなくなるので、パフォーマンス的にうまみが減ります。
基本的にはクライアント側へ遅延させるのを選ぶのが良いかと思います。
最終的に、部分的なハイドレーションに備えるには、ハイドレーションをどういうタイミングで、どう階層を踏んでやっていくかということを、コンポーネントの分割の単位、つまり境界で考えていく必要があります。ユーザーインターラクションの有無とか表示するデータの更新タイミングとかを前提にして考えていく必要があります。
これはデモの内容ですが、アプリケーションの外側がファーストビューで出てくるApp Shell的な即時読み込み領域(青)で、右側がAPIを呼び出した結果のデータによる、遅延可能なコンテンツ部分の領域(赤)です。どれだけ赤の部分を増やせるかで、ファーストビューの速さが変わってくるということです。
しかし、この中でも無駄なく、ユーザーが必要になった時にそこだけ遅延できる分界点をどれだけ作れるかが、ユーザーに必要なだけのJSを無駄なく届けるために重要な観点であり、コンポーネントの分割単位に直接関わります。
したがって、App Shell的に即時ハイドレーションされるヘッダーやナビゲーションなどはひたすら軽量に作っておく必要があるし、できるだけ広い範囲を遅延させられるようにし、その中で階層化して必要なものを必要なだけ送れるようにコンポーネント設計に反映していく必要があります。
まとめです。AngularもSSRが当たり前の時代にようやく追いついてきます。Googleの方針としても、SSRに振り切ったWizという別のフレームワークと、SSRが苦手ですが開発者体験を重視しているAngularをマージしようとしているので、どう考えてもSSRを強化していく方向に向かっています。より恩恵を受けるためにも、SSRに耐え得るアプリケーションに変えていく必要があり、そのために実行環境を意識した実装や設計にしていく必要があるし、部分的ハイドレーションに備えた遅延実行に耐え得るアプリケーションの分割点、分界点などを考えてより効果的にユーザー体験を良くしていく必要があるというのが今日のまとめです。以上です。