2023年5月7日 • ☕️ 4 min read
English

課題

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へ以下のように追加します。

pages/_app.tsx
Copy
// 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秒以上かかることがわかります。

getInitialProps-cold-start

起動済みのインスタンスが存在する際のアクセスでは、サーバーからのレスポンス時間が400ms前後になります。

getInitialProps-started

getServerSideProps(Serverless Function)

getServerSidePropsは、getInitialPropsと同じように実装できます。

pages/index.tsx
Copy
export const getServerSideProps = async () => {
  const dataRes = await fetch("https://jsonplaceholder.typicode.com/todos/1");

  return {
    props: {
      data: await dataRes.json(),
    },
  };
};

getInitialPropsと同じ、初回目のアクセス(Cold Start)に、サーバーからのレスポンス時間が2秒以上かかることがわかります。

getServerSideProps-cold-start

起動済みのインスタンスが存在する際のアクセスでは、サーバーからのレスポンス時間が400ms前後になります。

getServerSideProps-started

getServerSideProps(Edge Function)

Edgeレンダリングが有望な候補です。Edge Functionであればキャッシュがなくても起動時間はわずか80msほどで、実装も非常に簡単です。getServerSidePropsindex.tsxにレンダリング環境を指定するだけです。

pages/index.tsx
Copy
export const config = {
  runtime: "experimental-edge",
};

かなり改善されます。初回目のアクセスでも150ms前後になります。

getServerSideProps-edge-cold-start

すぐ再アクセスすると、50ms前後になります。

getServerSideProps-edge-started

appフォルダー(Serverless Function)

page.tsxにデータを取得する関数を追加して、React Server Componentを使えばいいです。

app/page.tsx
Copy
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の問題が解決されません。

app-cold-start

起動済みのインスタンスが存在する際のアクセスでは、サーバーからのレスポンス時間が400ms前後になります。

app-started

appフォルダー(Edge Function)

実装もServerless Functionとほぼ同じ、runtimeを指定すればOKです。

app/page.tsx
Copy
export const runtime = "edge";

初回目のアクセスは僅か200ms前後になります。

app-edge-cold-start

すぐ再アクセスすると、40ms前後になります。

app-edge-started

まとめ

初回目(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レンダリングを利用するのが良い選択だと思われます。


関連投稿

OpenAIを使ってNotionのページを検索する

2023年6月5日

VercelのServerless FunctionとEdge Functionのパフォーマンス比較

2022年12月25日

ThunderMiracle

Blog part of ThunderMiracle.com