課題
Next.jsといえば、サーバーサイドレンダリング(SSR)が特徴的ですね。SSRを利用すると、多くの場合Vercelでのデプロイが一般的となります。というのも、独自に構築するのは容易ではないからです。興味があれば、open-nextをご覧ください。
Vercelでのデプロイは便利ですが、問題も存在します。VercelのSSRはサーバーレスでレンダリングされるため、スケーラビリティは確かに優れています。しかし、サーバーレスにはCold Start問題があります、そのため初回アクセス時のレスポンスがかなり遅くなることがあります。この問題の解決策を探求してみました。
TL;DR
Next.jsのバージョン13.4から、app
フォルダーが正式に利用可能になりますが、サーバーレスファンクションのCold Start問題は解決されていないようです。キャッシュができず、毎回SSRが必要なページの場合は、Node.js
でのレンダリングではなく、Edge
でレンダリングしましょう。
古いgetInitialPropsはEdgeレンダリングが利用できないため、使用を避けることが望ましいです。
計測結果
前提条件
- リクエスト毎にSSRを実行し、キャッシュを行わない
- サーバー側の処理時間を最小限に抑える
- レスポンスのバンドルサイズを考慮しない
- Vercelのサーバーを
Tokyo-hnd1
に設定する
getInitialProps
実装はとても簡単です。_app.tsxへ以下のように追加します。
// use getInitialProps to fetch data
MyApp.getInitialProps = async (appContext: AppContext) => {
const appProps: any = await App.getInitialProps(appContext);
const { req } = appContext.ctx;
if (req) {
const host = req.headers.host;
const dataRes = await fetch("https://jsonplaceholder.typicode.com/todos/1");
return { pageProps: appProps.pageProps, host, data: await dataRes.json() };
} else {
const { host, data } = window.__NEXT_DATA__.props;
return { pageProps: appProps.pageProps, host, data };
}
};
初回目のアクセス(Cold Start)に、サーバーからのレスポンス時間が2秒以上
かかることがわかります。
起動済みのインスタンスが存在する際のアクセスでは、サーバーからのレスポンス時間が400ms前後
になります。
getServerSideProps(Serverless Function)
getServerSidePropsは、getInitialPropsと同じように実装できます。
export const getServerSideProps = async () => {
const dataRes = await fetch("https://jsonplaceholder.typicode.com/todos/1");
return {
props: {
data: await dataRes.json(),
},
};
};
getInitialPropsと同じ、初回目のアクセス(Cold Start)に、サーバーからのレスポンス時間が2秒以上
かかることがわかります。
起動済みのインスタンスが存在する際のアクセスでは、サーバーからのレスポンス時間が400ms前後
になります。
getServerSideProps(Edge Function)
Edgeレンダリングが有望な候補です。Edge Functionであればキャッシュがなくても起動時間はわずか80msほどで、実装も非常に簡単です。getServerSideProps
のindex.tsx
にレンダリング環境を指定するだけです。
export const config = {
runtime: "experimental-edge",
};
かなり改善されます。初回目のアクセスでも150ms前後
になります。
すぐ再アクセスすると、50ms前後
になります。
appフォルダー(Serverless Function)
page.tsx
にデータを取得する関数を追加して、React Server Componentを使えばいいです。
async function getData() {
const res = await fetch("https://jsonplaceholder.typicode.com/todos/1", {
cache: "no-store",
});
return await res.json();
}
export default async function Home() {
const dataPromise = getData();
const data = await dataPromise;
return (
<Suspense fallback={<div>Loading...</div>}>
<pre>{JSON.stringify(data)}</pre>
</Suspense>
);
}
初回目のアクセス(Cold Start)が相変わらず2秒ほど
かかります。React Server Componentでバンドルの問題が解決されるが、Cold Startの問題が解決されません。
起動済みのインスタンスが存在する際のアクセスでは、サーバーからのレスポンス時間が400ms前後
になります。
appフォルダー(Edge Function)
実装もServerless Functionとほぼ同じ、runtimeを指定すればOKです。
export const runtime = "edge";
初回目のアクセスは僅か200ms前後
になります。
すぐ再アクセスすると、40ms前後
になります。
まとめ
初回目(Cold Start) | すぐ再アクセス | |
---|---|---|
getInitialProps | 2s以上 | 400ms |
getServerSideProps(Serverless Function) | 2s | 400ms |
getServerSideProps(Edge Function) | 150ms | 50ms |
appフォルダー(Serverless Function) | 2s | 400ms |
appフォルダー(Edge Function) | 200ms | 40ms |
Webサイトの表示に3秒ほどかかると、離脱率が大幅に上昇する可能性があります。キャッシュを使用できないページの離脱率を軽減するために、トレンドとなっているEdgeレンダリングを利用するのが良い選択だと思われます。