
Next.jsのServerless FunctionのCold Start問題の改善
課題
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レンダリングを利用するのが良い選択だと思われます。

Blog part of ThunderMiracle.com
コメントは表示領域に入ると読み込みます