useAsyncData とそれを実現する Nuxt 3 の非同期処理の機構

Published: 2022/9/16


Nuxt 3 から、これまでの asyncDatafetch がそれぞれ useAsyncDatauseFetch に置き換わった。そして、 useFetch は内部で useAsyncData を利用するため、実質 useAsyncData のみで非同期機構が実現されている。

本質的に、 SSR / rehydrate の機構とページ遷移の機構について、フレームワークとしてのリアーキテクチャの結果であり、その機構についての自分の理解を dump する。

ページ遷移時の非同期処理のリアーキテクチャ

Nuxt 2 までは、 asyncData()fetch() を定義すると、 SSR 時にはそれらが完了するまでレスポンスコードの生成を await し、CSR (ページ遷移)時には asyncData についてはページ遷移のフックとして、 fetch については created でデータの取得を開始し、そのステータスをリアクティブに変更していくことで、非同期性を実現していた。

Nuxt 2 の問題点としては、 asyncData は必ず一つでなければならない制約、があった。 ページ遷移のコードにフックし、ページコンポーネントに asyncData が定義されていれば、それの取得完了を await してから、コンポーネント自身の data()asyncData() の戻り値を merge し、その結果の object を $data に代入する。 これを、クライアント側のページ遷移のフックとして記述するため、ページ用のコンポーネントを生成(render)する前にこのフックは実行されねばならず、なので必ず存在する Route 用コンポーネントである pages のコンポーネントのクラス定義に asyncData があるかどうかをチェックし、それが見つかれば実際にページ遷移の前にそれを実行するようにする。 なので、 render する前に asyncData の場所を特定するために、 Nuxt 2 においては asyncData が許容されるのは Route 直下の pages コンポーネントのみ、となっていた。

Nuxt 3 が利用する Vue 3 からは、 <Suspense> の機構が可能になった。 これは、その配下にあるコンポーネントのいずれかが非同期コンポーネントであったり async setup を持っていたりなどした場合に、その Suspence コンポーネント自体が待ちであるとして、任意の fallback コンポーネントを代わりに描写したり、 dom への反映を送らせたりする機能。 これを Vue Router の直下ぐらいに配置することで、 setup や非同期コンポーネントなどがいろいろあろうとも、それらがすべて完了するまでページの遷移をさせない、が可能になる。

まとめると、ページ遷移時の非同期処理の取り扱いが以下のように変わっている:

  • Nuxt 2: ページ遷移の middleware として asyncData の処理を挟み込み。 fetch については created() で発火するただの api call に帰着。
  • Nuxt 3: Vue3 の Suspense でページ遷移(dom 反映)を止めながら、実際に render / await をしていく。それらがすべて完了すると、 suspense の効果により dom が切り替わる。

クライアント asyncData/fetch の key によるキャッシュ化

SSR では、サーバー側で構築されていたコンポーネントインスタンス群をクライアント側でも再現するため、サーバーでの asyncDatafetch() の結果をクライアント側に serialize して流し、それを利用してクライアント側ではインスタンスをセットアップする、というパッチのような処理を行う。 とすると、どのようにシリアライズし、どのデータがどのコンポーネントに対応するかを判定する必要がある。 asyncData ならば Route Component の構造から自明であり、なので「ただひとつの asyncData の結果」をそのままシリアライズ・復元すれば良い。 問題は、実際に render してみないとどれだけの数が実行されることになるのかが分からない fetch() によるデータ群である。 (補足として、 Nuxt 2 における fetch() は、 fetch 完了後の data をシリアライズ対象とする。)

Nuxt 2 までは、まずデフォルトでは、各 fetch() はその実行順に採番されて、配列としてそのデータをシリアライズして html に埋め込む。 クライアント側は、 rehydrate の際に fetch があった順にその serialize の結果を代入していく。 ただ、これは例えば Full-static な SSG するような場合で、クライアント側のコンポーネントの状態によって生成されるコンポーネントが異なり、結果 fetch の順番がズレるなどすると、動作が怪しくなってくる。 これに対応するため、 Nuxt 2.15 から、各フェッチを行うインスタンスにキーを任意に自分で指定できるようになり、必要であればこれをチューニングする形になっている。 いずれにせよ、 key は rehydration / SSG のために利用されている。

Nuxt 3 からは、より抜本的に非同期データの key による保存・再取得が行われる。 一般的なサーバーサイドプログラミングにおけるキャッシュキーに近い。 クライアント側にて rehydrate 処理のための App 構築最中であったり、ページ遷移の際に、実行されることになる useAsyncData (と、それを利用する useFetch) がある場合には、そのキーに対応するデータが、クライアント側のグローバルな asyncData の置き場に無ければ、その時点でデータを async で取得するような構造になっている。 SSR して返ってくるサーバー側のデータは、なので、 SSR で利用した useAsyncData たちのデータを、キーバリュー形式で一つの JSON にまとめて、 html に埋め込んでそれをリターンする。 クライアント側ではまずサーバー側で計算されたこれら値でもって、グローバルな asyncData の結果置き場を初期化する。 これにより、もし対象の useAsyncData (のキー) が既に計算されているならばその結果のみ即座に利用することで、不必要な非同期処理を根本的に抑制できる。 (Vue 2 時代にはページ遷移の度に asyncData が強制的に実行/await されていた。一方 nuxt 3 では、ページ遷移であっても、すでにその key がグローバル asyncData 置き場にあれば、それを再利用する。) ただしその代わりに、より useAsyncData の制御が(慣れないと)難しくなっている。

まとめ

  • Nuxt2 まで: SSR した結果を key を通じて rehydration (の前の App 構築)のタイミングで代入する。それ移行 SPA として振る舞っている間は特に利用されない。
  • Nuxt3 から: クライアント側のすべての useAsyncData は key-value のキャッシュ的にふるまう。SSR の結果でもってこのキャッシュは初期化される。

総括

Nuxt 3 の SSR / rehydrate / ページ遷移の機構は suspense がキーとなって完全にリアーキテクチャされている。 Nuxt 3 の世界においては、

  • useAsyncData の結果は本質的にキャッシュであり、それをサーバー側で計算するかクライアントで計算するかの違いでしかなく、かつ、すでに key に対応するデータがあるならばそれを使いまわす
  • ページ遷移においては page コンポーネント(route コンポーネント)だけを特別扱いするのではなく、普通にすべての useAsyncData を同じように扱いながら、非同期によるページ遷移のブロック自体は Vue 3 の Suspense に任せる

ことにより、不必要な async 実行を避け、効率的な SPA が実現される。

Summary in FAQ


Tags: nuxtnuxt3

関連記事