📝

Angular RFC: Deferred Loadingの翻訳

2023/10/04に公開

本記事は、 [Complete] RFC: Deferred Loading #50716 の翻訳となります。RFCの翻訳ってあまりみないので、翻訳してみました。もうこれは Complete で来月にはリリースされる機能なのですが、AngularはDiscussionsで将来的な仕様について議論を行っているので、定期的にのぞいてみると面白いかもしれません。


[Complete] RFC: Deferred Loading #50716

現代のウェブでは、アプリケーションのロード中に優れたユーザー体験を提供することを重視しています。コアウェブバイタルのようなメトリクスは、この体験を定量化し、初期ロードのパフォーマンスの重要性を強調します。

この初期ロードの体験を最適化するための1つの標準的で効果的なテクニックは、UIの優先順位の低い部分を遅延し、ページの最も重要な部分のロードにリソースを集中させることです。例えば、メインの動画とコメントのリストを表示するページでは、最初に動画をロードし、動画が完全にバッファリングされて再生準備が整うまでコメントをレンダリングするコードのロードを延期するように最適化できます。

Angularは、ルータ(lazy routes)を介して、アプリケーションの一部を遅延ロードすることをサポートしています。個々のコンポーネントの遅延ロードは、 dynamic import()ngComponentOutlet で実現できますが、このアプローチは複雑でエラーが発生しやすくなります。

そのため、私たちはコアフレームワークに、クライアントサイドとサーバーサイドレンダリングの両方で動作する、遅延ロードへの人間工学的で全体的なアプローチを導入することに多くの可能性を感じています。このRFCは、新しいフレームワークプリミティブ: {#defer} を提案します。

タイムラインに関するメモ

私たちはこのRFCを、初期レンダリングとサーバーサイドレンダリングに関するより大きな取り組みの一環として実施しています。私たちはここで提示されたアイデアに興奮していますが、私たちの目標は、私たちのロードマップに情報を提供するための初期のフィードバックを収集することであり、他のプロジェクトが完了するまで実装フェーズに進まないかもしれません。実装を開始する場合は、時間をかけて徐々に機能を追加していく予定です。

どのようなフィードバックを求めていますか?

このAngularの遅延読み込みのプリミティブについて、あらゆるコメントをお待ちしています。コメントを残す際には、回答する公開質問(注:Discussion Question)を必ず明記してください。

いつも通り、私たちの行動規範を念頭に置いてください。このような範囲の提案は、Angularとプロジェクトでの使用方法に大きな影響を与えることを私たちは知っています。私たちはこのプロジェクトに対するあなたのコミットメントと投資に感謝し、コメントを尊重するようお願いします。

目標

  • Angularの遅延ロードを予測可能にし、すべての開発者にとって人間工学的なものにする。
  • 初期バンドルサイズを小さくすることで、ページの初期ロード時間を短縮する。 コンポーネントだけでなく、ディレクティブやパイプの遅延ロードをサポートする。
  • 開発者が遅延ロードを活用したアプリケーションを構築できるように、新しいパターンを導入する
  • 遅延ロードされたプリミティブをハイドレーションと統合し、開発者が最大限の利益を得られるようにする

遅延

Deferred blocks

(Note: この遅延ロードプリミティブは、コントロールフローRFCで提案された新しいブロックテンプレート構文を利用しています。)

このプリミティブは、テンプレートでは以下のブロック群で表現されます: {defer}...{/defer}。遅延ブロック内で参照される依存関係(コンポーネント、ディレクティブなど)は遅延ロードされます。その結果、遅延ブロックは常に非同期にレンダリングされます。これには、遅延ブロック内のすべての依存関係が含まれ、コンポーネント、ディレクティブ、依存関係内で使用されるパイプが含まれます。

{#defer}
  <calendar-cmp />
{/defer}

on と when

概念的には、defer ブロックは、トリガーされたときに、プレースホルダのコンテンツを遅延ロードされたコンテンツに置き換える構成です。開発者は、この入れ替えがいつトリガーされるかを設定するために、 onwhen の2つのオプションを使えます。

when は、真偽値を返す式の形で命令条件を指定します。この式がtrueになると、プレースホルダは遅延ロードされたコンテンツと入れ替わります(依存関係をフェッチする必要がある場合は、非同期操作になるかもしれません)。

重要なのは、when 条件がfalseに戻っても、deferブロックはプレースホルダに戻されないことです。スワップは1回限りの操作です。ブロック内のコンテンツが条件付きでレンダリングされるべき場合は、ブロック自体の中で if 条件を使用することができます。

on は、宣言的なトリガー条件(通常は何らかのイベント)を指定します。あらかじめ定義された一連のイベント・トリガーを用意します(トリガーの種類のセクションを参照)。例えば、on interactionon viewport などです。

一度に複数のイベントトリガーがサポートされます。例えば、 on interaction, timer(5s) は、ユーザーがプレースホルダを操作した場合、または5秒後にdeferブロックがトリガーされることを意味します。

より詳細な例を以下に示します。

{#defer when cond}
  <calendar-cmp />
{/defer}
{#defer on interaction, timer(5s)}
  <calendar-cmp />
{/defer}

別の方法として、whenとonの両方を1つのステートメントで一緒に使い、どちらかの条件が満たされた場合にスワップがトリガーされるようにすることもできます。

{#defer when cond; on interaction}
  <calendar-cmp />
{/defer}

:placeholder ブロック

デフォルトでは、deferブロックはトリガーされる前にコンテンツをレンダリングしません。プレースホルダブロックを使用すると、開発者は defer ブロックがトリガされる前に表示されるべきコンテンツを指定することができます。

このブロックには、DOM ノード、コンポーネント、ディレクティブ、パイプなど、どのようなコンテンツを指定してもかまいません。

{#defer when cond}
  <calendar-cmp />
{:placeholder minimum 500ms}
  <img src="placeholder.png" />
{/defer}

Note: :placeholder ブロックの依存関係は、早期ロードされ、遅延されません。

:placeholder ブロックは省略可能ですが、特定のon条件に該当しない、空でないプレースホルダが必要になります。さらに、 minimum 条件を指定することもできます。これにより、次のテンプレートに切り替える前にプレースホルダテンプレートを表示する最小時間を指定できます。これにより、トリガー状態が即座に発生し、応答がわずかな時間しかかからなかった場合に発生する可能性のある、高速なちらつきを防ぐことができます。

:loading ブロック

:loadingブロックは、deferブロックがそのdeferテンプレートをレンダリングするために依存関係をフェッチしている間に表示されるべきコンテンツを定義します。上記の :placeholder ブロックと同様に、このブロックはオプションであり、提供されない場合、deferブロックは deferコンテンツがロードされるまで、(もしあれば) :placeholder ブロックを表示し続けます。

{#defer}
  <calendar-cmp />
{:loading}
  <div class="loading">Loading the calendar...</div>
{/defer}

Note: :placeholder と同様に、 :loading ブロックの依存関係は早期ロードされます。

:loading ブロックを使用する場合、 after 条件を指定することもできます。これにより、ローディングテンプレートを表示する前に待つ最小時間を指定することができます。ロードを延期するリクエストにかかった時間が指定された時間より短い場合、ローディングテンプレートは表示されません。これはローディングテンプレートと遅延テンプレートの間の高速なちらつきによって引き起こされるレイアウトのスラッシングを防ぐためです。

:placeholder と同様に、 minimum 条件を指定することができます。これにより、コンテンツに切り替える前にローディング・テンプレートを表示する最小時間を指定できます。これにより、ローディングがわずかな時間しかかからなかったり、アフターコンディションが終了してからの時間が短かったりした場合に発生する可能性のある、高速なちらつきを防ぐことができます。

{#defer}
  <calendar-cmp />
{:loading after 100ms; minimum 500ms}
  <div class="loading">Loading the calendar...</div>
{/defer}

:error ブロック

:error ブロックは、遅延ローディングが失敗したときにレンダリングされるべきUIを表します。上記のプレースホルダブロックやローディングブロックと同様に、このブロックはオプションであり、提供されない場合、deferブロックはコンテンツをレンダリングしません。

{#defer}
  <calendar-cmp />
{:error}
  <p> Failed to load the calendar </p>
  <p><strong>Error:</strong> {{$error.message}}</p>
{/defer}

Note: :placeholder:loading と同様に、 :error ブロックの依存関係は早期ロードされます。

:error ブロックとともに、ユーザーは :error ブロックでアクセス可能な $error 変数を通して、ロード中に投げられたエラーにアクセスすることができます。

Discussion Question 1B: シグナルやイベントを介して、deferブロックの状態が変化したときに、それを観察できるようにする必要があると思いますか?

リソースのプリフェッチとコンテンツのレンダリング

deferされているローディングアクションをコンテンツレンダリングアクションから分離したいという要望があるかもしれません。例えば、ユーザは予想されるユーザアクションに備えて依存関係のセットをプリフェッチし、遅延ブロックが対話可能になるまでの遅延を減らしたいかもしれません。その場合、ユーザは、メイントリガの前に依存関係をプリフェッチするために、オプションでプリフェッチ条件を提供することができます。prefetch構文は、メインのdefer条件と同様に動作し、トリガを宣言するためにwhenやonを受け入れます。

この場合、 defer に関連付けられた whenon は、いつレンダリングするかを制御し、 prefetch whenprefetch on は、いつリソースをフェッチするかを制御します。これにより、例えば、ユーザーが実際に defer ブロックを見たり操作したりする前に、リソースのプリフェッチを開始することができます。

on やdeferの when と同様に、これらは一方通行の動作です。 prefetch when をfalseに切り替えてもコンテンツは隠れません。コンテンツの非表示は、if で構成すると実現できます。

{#defer on interaction; prefetch on idle}
  <calendar-cmp />
{/defer}

ブラウザがサポートしている場合は、ブラウザのnavigator.connection data、NetworkInformation.saveData、NetworkInformation.effectiveTypeを調べることで、現在のブラウザの設定とユーザーの接続速度を考慮する予定です。これにより、プリフェッチに関して可能な限り最高のユーザーエクスペリエンスを提供できるようになります。

タイムアウト

Deferブロックは、開発者が遅延ロード操作のタイムアウトを設定するためのAPIを提供します。

この動作を設定するには、タイムアウトを {#defer} ブロックで定義します:

{#defer timeout 100ms}
  <calendar-cmp />
{:error}
  ...
{/defer}

Discussion Question 2B:アプリケーション内の異なる遅延ブロックが異なるタイムアウトを使用すべきユースケースはありますか?

トリガーの種類

idle

idle は、ブラウザがアイドル状態になったら遅延ロードをトリガします。これはdeferブロックのデフォルトの動作です。

interaction

interactionとは、クリック、フォーカス、タッチ、入力イベント(キーダウン、ブラーなど)を意味します。

immediate

クライアントがレンダリングを終了すると、deferチャンクはすぐにフェッチを開始します。

timer(x)

timer(x)は指定された時間の後にトリガされます。

hover

hover は、マウスがトリガー領域(プレースホルダコンテンツまたは渡された要素参照)の上に置かれたとき、遅延ロードをトリガーします。

viewport

viewportは、IntersectionObserver APIを使用して、指定されたコンテンツがビューポートに入ったときに遅延ブロックをトリガします。これはプレースホルダコンテンツまたは要素参照となります。

Example #1: 遅延ロードは、プレースホルダコンテンツがビューポートに表示されたときにトリガされます。

{#defer on viewport}
  <calendar-cmp />
{:placeholder}
  Calendar placeholder
{/defer}

Example #2: deferされたローディングは、他の要素がローカル参照を使ってビューポートに表示されるようになったときにトリガーされます:

<div #greeting>Hello!</div>

{#defer on viewport(greeting)}
  <greetings-cmp />
{/defer}

これらのトリガーは、最も一般的なシナリオを扱うように設計されています。もし、あなたのユースケースが、定義済みのトリガーリストよりも複雑なシナリオを必要とする場合は、when を使って条件を定義してください。

Discussion Question 3B:この定義済みトリガーのリストは、ユーザーが持つ可能性のあるケースの大部分を処理するのに十分ですか?この設計で考慮しなかった、他の一般的なトリガータイプを思いつきますか?

Discussion Question 4B: アプリケーション内のすべての {#defer} ブロックについて、デフォルトのトリガー動作をグローバルに変更する必要があるようなユースケースを思いつきますか?

入れ子

{defer} ブロックは入れ子にすることができます。例えば、ビューポートに表示されたときに大きなコンポーネントをレンダリングし、ユーザがボタンをクリックしたときに内部のコンポーネントをロードしたい場合があります。

重要な注意: deferセクションを入れ子にすると、Angularがレンダリングを進め、より多くのdefer境界を発見するにつれて、コンポーネントが余分なリソースを順次読み込むというパフォーマンスの問題が発生する可能性があります。この問題を回避するための診断とツールを開発者に提供する予定です。以下の「カスケードリクエスト」のセクションを参照してください。

サーバーサイドレンダリングの動作

サーバー上でアプリケーションをレンダリングするとき、deferブロックは常にそのプレースホルダをレンダリングします(プレースホルダが指定されていない場合は何もレンダリングしません)。トリガーはサーバー上では無視されます。

defer とサーバサイドレンダリングの統合に関する今後の計画については、次のセクションを参照してください。

部分的なハイドレーションへの将来の可能性

サーバサイドレンダリングに関する私たちのロードマップには、部分的なハイドレーション(最初のページロード後にサーバサイドレンダリングコンテンツを脱水したままにすること)をサポートするためにハイドレイティングの実装を拡張する研究が含まれています。

パーシャル・ハイドレーションを有効にすると、遅延されたコンテンツはサーバーレンダリングされ、トリガー条件が満たされるまで脱水されたままになります。私たちは、この領域でさらに研究を行うことを計画しており、将来のRFCで、より詳細な方法でサーバレンダリングとハイドレーションの動作を設定できるように、潜在的にdeferブロックに追加設定をもたらす可能性があることをフォローアップする予定です。

よくある落とし穴

リクエストのカスケード

複数のdeferブロックを入れ子にすると、リクエストのカスケードが発生する場合があります。この例は、即時トリガーを持つ defer ブロックが、別の即時トリガーを持つ defer ブロックをネストしている場合です。これがすべて1つのテンプレート内であれば、それらを1つのリクエストに簡単にまとめることができますが、ネストされたコンポーネントテンプレート内のこのケースでは、これは捕捉できないでしょう。このようなパターンについて開発者に警告するために、特別な診断を実装する予定です:

一つのテンプレート内で入れ子になったdeferブロックに対して、コンパイラーはそれらのパターンを検出し、ユーザーに警告を出力することができます。これは、拡張テンプレート診断サブシステムでも処理できます。

複数のコンポーネントにまたがるネストしたブロックについては、実行時に検出メカニズムを追加することができます(開発モードのみ)。

これはまた、アンチパターンとして(他のグッドプラクティスやバッドプラクティスとともに)ドキュメントで強調されるでしょう。

defer ブロック内のコンテンツ投影

コンポーネントの外側から投影されたコンテンツの依存関係は、deferブロックに投影されたときには遅延されません。投影されたコンテンツの依存関係を遅延ロードしたい場合は、そのコンテンツを {#defer} ブロックに(必要な条件付きで)別途ラップします。

依存関係のないコンテンツのラップ

ユーザーが {#defer} ブロックを書き、その defer ブロックの中に、抽出する依存関係(コンポーネント、ディレクティブ、パイプなど)を持たないコンテンツだけがある場合、 defer ブロックは実際には何の役にも立ちません。このような場合を検出して、ユーザーにエラーを与えるようにしましょう。defer ブロックの中にコンテンツが投影されるケースは、その一例です。ユーザがDOMを内部に配置するだけで、ディレクティブが何も適用されていない場合は、別のケースになります。これはdeferブロックの不要な使用であることをドキュメントで強調することにします。また、ユーザがこのような間違いをしないように、コンパイル時のチェックを追加したいと思います。

累積レイアウトシフト(CLS)のリスク

フォールドの上に {#defer} ブロックを使用する場合、プレースホルダがそれを置き換えるディファード・コンテンツの正確なサイズと一致していないと、アプリケーションのコアとなるウェブ・バイタルの累積的なレイアウト・シフトの一因となります。厳密にはCWVの観点から、一般的に推奨されるのは、a)ビューポートの外にコンテンツを挿入するか、b)ユーザーの入力に応答してコンテンツを挿入することです。

よくある質問

Q: この機能はなぜdeferと呼ばれているのですか?

これについては、FAQの後半まで回答を先延ばしにしています。

Q: なぜこの機能はdeferと呼ばれるのですか?

この機能が行っている動作は、最初のページロード後に特定の条件が満たされるまで、何かのロードを延期するというものです。これはスクリプトのdefer属性に似た動作で、並行してリソースを読み込み、ページが解析された後に実行されます。遅延読み込みは、必要なときだけ読み込むことです。遅延ローディングは、単に遅延的に何かをロードする以外にも使用することができます。

Q: 遅延ブロックの中で使えますか?

はい。ただし、deferブロックの中だけで使うことはできません。詳しくは上記の「依存関係のないコンテンツのラップ」のセクションを参照してください。

Q: 遅延ロードを命令的にトリガーすることはできますか?

when式を使用して、任意の時点で遅延ブロックをトリガーすることができます。

Q: deferブロックがロードされたとき、またはステートが変更されたときを観察する方法はありますか?

question 1Bを参照してください。

Q: 独自のトリガーを定義できますか?

いいえ、ユーザー定義のトリガーをサポートする予定はありません。現在の宣言型トリガーのセットは、フレームワークに緊密に統合される予定です。もしあなたのアプリケーションで有用な新しいトリガーのアイデアがあれば、ぜひお聞かせください(質問4Bを参照)。

また、when 式を使ってトリガーロジックを完全にカスタマイズすることもできます。

Discussion