Nuxt3のuseFetchとawait
Nuxt3の useFetch
は 以下のようなPromiseオブジェクトを返します。
Promise<AsyncData<DataT, ErrorT>>
Promiseオブジェクトであるため async
/ await
が利用可能です。
Nuxt.jsはTop-Level Awaitに対応していますので、<script setup>
のルートではasync宣言をしなくてもawait
が利用可能です。
<template>
<pre>{{ data.time }}</pre>
</template>
<script setup>
const { data } = await useFetch('/api/lazy');
</script>
awaitは処理をブロックする
JavaScriptの基本的なおさらいから入りますがawait
は非同期処理が解決されるまでJavaScriptの処理をストップします。
実際の挙動を見ていきましょう。
まずは、server/api/lazy.ts
として以下のレスポンスに2秒ぐらいかかかるAPIを用意します。
export default defineEventHandler(async () => {
const time = new Date()
await new Promise(resolve => setTimeout(resolve, 2000));
return {
time
}
})
次に、pages/lazy.vue
として複数回APIにアクセスしているページを用意します。
pages/lazy.vue
ではAPI通信ごとに何秒かかったかをconsole.timeLog()
で出力しています。
<template>
<pre>{{ data1.time }}</pre>
<pre>{{ data2.time }}</pre>
<pre>{{ data3.time }}</pre>
<pre>{{ data4.time }}</pre>
</template>
<script setup>
console.time()
const { data: data1 } = await useFetch('/api/lazy')
console.timeLog()
const { data: data2 } = await useFetch('/api/lazy')
console.timeLog()
const { data: data3 } = await useFetch('/api/lazy')
console.timeLog()
const { data: data4 } = await useFetch('/api/lazy')
console.timeEnd()
</script>
SSRのログを確認すると以下のようになっています。
default: 0.004ms
default: 2.003s
default: 4.006s
default: 6.010s
default: 8.015s
ページ表示の処理が終わるまで8秒ほどの時間がかかったのがわかります。
すべての通信を同期通知に変換してしまうとページ表示まで時間がかるため、通信は非同期処理として並列にAPIアクセスするようにしておきたいです。
Promise.allで解決する
一般的なJavaScriptアプリケーションではこの問題を解決するためにPromise.allを活用します。
Nuxt.jsではもう少しスマートな解決方法が用意されていますのでその方法は後ほど解説します。
以下のようにPromise.all
の引数にPromiseオブジェクトを配列で指定することで、すべての非同期処理として並列におこないすべての非同期処理がおわるまでまって処理を再開します。
<template>
<pre>{{ data1.time }}</pre>
<pre>{{ data2.time }}</pre>
<pre>{{ data3.time }}</pre>
<pre>{{ data4.time }}</pre>
</template>
<script setup>
console.time()
console.timeLog()
const response = await Promise.all([
useFetch('/api/lazy'),
useFetch('/api/lazy'),
useFetch('/api/lazy'),
useFetch('/api/lazy')
])
const [
{ data: data1 } ,
{ data: data2 } ,
{ data: data3 } ,
{ data: data4 }
] = response
console.timeEnd()
</script>
SSRのログを確認すると以下のようになっています。
default: 0.004ms
default: 2.007s
ページ表示の処理が終わるまで2秒で済んだのがわかります。
Promise.all
はパフォーマンスの問題を解決しますがコードが複雑になってしまうのが難点です。
この問題はNuxt.jsではもうすこしスマートに解決することができます。
awaitを取る
Nuxt.jsでは当初のコードからawait
を取るだけでPromise.all
と同様の挙動にすることができます。
<template>
<pre>{{ data1.time }}</pre>
<pre>{{ data2.time }}</pre>
<pre>{{ data3.time }}</pre>
<pre>{{ data4.time }}</pre>
</template>
<script setup>
console.time()
const { data: data1 } = useFetch('/api/lazy')
console.timeLog()
const { data: data2 } = useFetch('/api/lazy')
console.timeLog()
const { data: data3 } = useFetch('/api/lazy')
console.timeLog()
const { data: data4 } = useFetch('/api/lazy')
console.timeEnd()
</script>
このスクリプトのSSRのログを確認すると以下のようになっています。
default: 0.005ms
default: 0.432ms
default: 1.012ms
default: 1.457ms
default: 1.99ms
<script>
の処理時間はほとんど発生していないことが確認できます。
では、即座にページが表示されるかというとそうではなくて、Nuxt.jsではuseFetch
で発生した非同期通信が解決されるまでページの描画は行いません。
そのためページの描画はPromise.all
は利用した場合と同じぐらいの2秒ほどになります。
Promise.all
と比較すると直感的なコードのまま非同期通信を利用できるようになったと感じるでしょう。
useFetchのawait lessの注意点
await
を使わずにuseFetch
を利用した場合はlazyオプションを利用した場合とほぼ同じ挙動になのでその点は注意が必要です。
lazyオプションをつけたときと同様にSSR時は非同期通信が解決されるまでページの描画は行いませんがページ遷移などのCSR時は非同期通信が解決される前にページの描画を行ってしまいます。
以下のような遷移元のページを用意して
<template>
<NuxtLink to="/lazy">表示の遅いページに遷移</NuxtLink>
</template>
pages/lazy.vue
にアクセスすると以下のようなエラーが表示されます。
これはAPIのデータの取得が完了していないタイミングでページを表示したためdata1.time
なんてないよと怒られてしまっています。
これにはいくつかの解決方法があります
返却値ではオプショナルチェーンを利用する
非同期通信が解決されるまで返却値はnull
であるためdata1?
という具合に表示時にオプショナルチェーンを利用すると解決が可能です。
<template>
<pre>{{ data1?.time }}</pre>
<pre>{{ data2?.time }}</pre>
<pre>{{ data3?.time }}</pre>
<pre>{{ data4?.time }}</pre>
</template>
条件分岐を行う
数が多い場合はオプショナルチェーンを利用するのは手間でしょう。
その場合は直前にv-if
で存在の確認をするのがよいでしょう。
<template>
<pre v-if="data1">{{ data1.time }}</pre>
<pre v-if="data2">{{ data2.time }}</pre>
<pre v-if="data3">{{ data3.time }}</pre>
<pre v-if="data4">{{ data4.time }}</pre>
</template>
このコードの例がAPIのレスポンスが1データしかないので微妙ですが以下のように複数の値を参照しているときに有効です。
<template>
<div v-if="data">
<h1>{{ data.title }}</h1>
<p>{{ data.description }}</p>
<p>{{ data.time }}</p>
...
...
<p>{{ data.author }}</p>
</div>
</template>
結論
Nuxt.jsでページを作成する場合は必要がなければawait
はなしでuseFetch
を利用するのがよいでしょう。
ただし、レスポンスがリアクティブなデータで値が入る前まではnull
であることは注意が必要です。
Discussion