VercelのEdge Cacheの動作をNext.jsと比較する事で、Nuxt3のパフォーマンスチューニングを考察してみた。
SSRを利用したフロントエンドフレームワークでのパフォーマンスチューニング
NextやNuxt3等のSSRを利用したフレームワークを使ってアプリケーションを運用するにあたって、その真価を発揮するためには何をどこまで静的コンテンツとして確定させ、CDNにキャッシュさせるかを考慮する必要があります。Nextではデフォルトで全てのページがプリレンダされていて(事前にReactのコンポーネントがHTMLとして出力されていて)、コンテンツの表示にAPIのfetchが必要なページもgetStaticPathsとgetStaticPropsを使用することによって、静的なコンテンツとしてプリレンダする事ができます。(NextのドキュメントではSGという用語が使われていますが、コンテンツをリクエスト時ではなく事前にサーバーサイドでレンダリングしておくという意味では、SGもSSRを利用した機能の一種と捉える事ができます。)
VercelではEdge NetworkでのStatic cachingが自動で有効になっていて、特に意識をしなくてもプリレンダされた静的なファイルはStatic Files Cacheingによってキャッシュされるので、簡単にEdge Cacheを利用したアプリケーションの運用が可能となっています。
↓ビルド時にプリレンダしたHTMLがキャッシュされるレイヤー
出展: https://vercel.com/docs/concepts/functions/serverless-functions
前置きが長くなりましたが、NuxtでもNextのようにVecelのEdge Cacheを有効活用してアプリケーションを運用するにはどうすればいいか、Nextとの違いを比較しながら考察して行きたいと思います。
NextとNuxt3のEdge Cacheの動作を比較
VercelにデプロイされたアプリケーションでEdge Cacheがどこまで活用されているかどうかは、実際にNextやNuxt3のアプリケーションにアクセスして、レスポンスのx-vercel-cacheヘッダを見れば分かります。
そこで、公式のテンプレートを使ってNextとNuxt3のEdge Cacheの動作の比較をしてみます。
比較に使用するテンプレート
Nuxt3
Next①トップページのHTMLを返却するレスポンスの比較
まずは、Nodeサーバーへの最初のリクエストに対するレスポンスの比較します。
Nuxt3の場合
x-vercel-cacheがMISSとなっている↑
Nextの場合
x-vercel-cacheがHITとなっている↑
Nextではx-vercel-cacheがHITになってるものの、Nuxt3では何回ページをリロードしてもMISSとなります。この理由は後で考察しますが、Nuxt3のテンプレートではトップページのHTMLはEdgeでキャッシュされてない事が分かります。
②assets(バンドルされたJSやCSSなどの静的ファイル)のレスポンスの比較
Nuxt3の場合
Nextの場合
どちらもx-vercel-cacheがHITとなっていて、Edge CacheにHITしている事が分かります。
結論、JSやCSSなどのassetsはNuxt3でもNextでもEdge Cacheが効いている一方で、トップページのHTMLはNextでは効いているキャッシュがNuxt3では効いていないようです。
なぜNuxt3ではHTMLはEdge Cacheされないのか
Nuxt3は(Nextのように)事前にHTMLとしてコンポーネントをレンダリングしてサーバ上に出力する動作をせず、デフォルトではSSRになっているのが理由ではないかと推測していて、テンプレートをデプロイするとdashbordで出力にHTMLが無いことを実際に確認する事ができます。(ただ、キャッシュする層からみたら事前に出力されていたHTMLのレスポンスにしろ、オンデマンドでSSRによって出力したHTMLにしろどちらもHTMLなので、キャッシュするかどうかの判断に関しては気になるなと思いました。)
比較
Nuxt3の場合
cssとjsのファイルのみで、トップページのHTMLらしきものは見当たりません。
Nextの場合
indexというトップページのHTMLっぽいのが見つかり、実際に中を見てみるHTMLのファイルとなっている事が分かります。
以上から、Nextではデフォルトの設定で、プリレンダされたページがEdge CacheにHITする一方で、Nuxt3ではそうではない事が分かります。
Rendering on CDN Edge Workers
しかし、pagesディレクトリ配下でRoute Rulesでswrもしくはstaticに設定されているrouteで適用されるESR(Edge Side Rendering)を利用することで、ESRが適用されたページではNuxt3でもHTMLがFunction ResponsesとしてEdge Cacheされるようです。
↑Serverless Functionsのレスポンスがキャッシュされるレイヤー
実際の設定ファイル
ポイントはdefineNuxtConfigを使う事です。
export default defineNuxtConfig( {
pages: true,
routeRules: {
'/**':{static:true},
'/posts/1':{static:true},
'/posts/**': {static:true},
}
})
Outputを確認しすると、/**に対して__nitro-、/posts/1に対して1、/posts/**に対して__nitro--postsという名前のファイルでEdge Functionが生成されている事が確認できます。
↓実際にEdge Cacheを確認したリポジトリです。
useFetchを使ったページでstatic(swr)を使うとどうなるか?
NuxtでもESRを使うことでHTMLをEdge Cacheから返す事ができることを確認できました。ただし、NextでgetStaticPathsとgetStaticPropsを使う場面のように、コンテンツの表示にfetchが必要なページではAPIのレスポンスを含んだHTMLを生成し、キャッシュする必要があります。
そこで、Nuxt3でuseFetchを使ったページをstaticにした場合の動作を確認してみます。
<script setup lang="ts">
const { data } = useFetch('/api/hello')
</script>
<template>
<NuxtWelcome />
<p>{{data.api}}</p>
</template>
export default defineEventHandler((event) => {
return {
api: 'works'
}
})
結果
レスポンスのHTMLにAPIの結果も含まれています。(もちろんx-vercel-cacheもHITとなっています)
useFetchが含まれているコンポーネントの場合、Nuxt3のESRではuseFetchのレスポンスを含んだ形でオンデマンドでページを生成し、結果をキャッシュしているようです。
まとめ
Nuxt3はStable版がリリースされたばかりで、情報の量もNextに比べるとまだまだという印象ですが、ESRによってEdge Cacheを活用する事でより高いパフォーマンスを発揮する事ができるのではないかと思います。特に、nuxt.config.tsでワイルドカードを使ってESRをしたいルートを設定さえしておけば、NextのようにgetStaticPathsやgetStaticPropsを使わなくても、APIのレスポンスを含んだページをEdge Cacheする事ができるのはNextにも負けない良さなのではないかと思いました。最後に、まだ自分もNuxt3を使い始めたばかりで、不正確な情報もあるかもしませんが、ここまで読んで頂きありがとうございました!
Discussion