🍪

Next.js(AppRouter)でDynamicFunctionを使ってもCacheHitさせる

2024/01/26に公開2

ResponseHeader
ResponseTime

はじめに

Next.js(AppRouter)をみなさん上手く扱えていますでしょうか?
弊社では現在新規プロダクトで使っているのですが、パフォーマンスが良くないという問題にぶちあたっていました。
今回はその解決方法を一部共有したいと思います。
前提条件として弊社は以下の環境となっています。

next : 14.1.0
react : 18.2.0
react-dom : 18.2.0
typescript : 5.3.3
Node.js : 18.17.1

デプロイ環境 : Vercel

問題

Next.js(AppRouter)では、headerscookiesといったDynamicFunctionが提供されています。
https://nextjs.org/docs/app/building-your-application/rendering/server-components#dynamic-functions

Using any of these functions will opt the whole route into dynamic rendering at request time.

ドキュメントにも書いてあるのですが、DynamicFunctionを利用すると動的レンダリングになってしまいます。
せっかくgenerateStaticParamsで静的なページを作成していても、そのページでheaderscookiesが使われていると毎回サーバーサイドレンダリング(以下SSR)が行われてしまいページのレスポンスが非常に遅い状態(2~3s)でした。

解決方法

方針としては以下になります。

  1. generateStaticParamsで生成された静的ページを返却するためにcookiesの呼び出しをコンポーネントから取り除く
  2. cookiesを使う処理をServerActionsに移行する
  3. cookiesによって動的に表示を変えたい部分はClientComponentにて適宜変更する

generateStaticParamsで生成された静的ページを返却するためにcookiesの呼び出しをコンポーネントから取り除く

page.tsxServerSideComponent等で以下のようにcookiesを呼び出すと思いますが、

const cookieStore = cookies()
const cookieHoge = cookieStore.get("hoge")
const cookieFuga = cookieStore.get("fuga")

これらを一旦削除します。

cookiesを使う処理をServerActionsに移行する

https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations
初回レンダリングでは静的に生成されたものを表示し、useEffectにてServerActionsを呼び出し再度cookiesを使った情報で表示し直すようにします。

"use server"
export async function handleChangeList() {
  const cookieStore = cookies()
  const hoge = cookieStore.get("hoge")
  const fuga = cookieStore.get("fuga")
  if (hoge === null || fuga === null) {
    return { null, "", "" }
  }
  
  const { hogeFugaList } = await fetchHogeFuga({
    hoge,
    fuga
  })

  return { hogeFugaList, hoge, fuga }
}
export async function saveHogeToCookies(value: string) {
  cookies().set("hoge", value)
}
export async function saveFugaToCookies(value: string) {
  cookies().set("fuga", value)
}

cookiesによって動的に表示を変えたい部分はClientComponentにて適宜変更する

"use client"
import { useEffect, useState } from "react"

type Props = {
  hogeFugaList: HogeFuga[]
}

export default function HogeFugaList({ hogeFugaList }: Props) {
  const { stateHogeFugaList, setHogeFugaList } = useState(hogeFugaList)

  useEffect(function () {
    void handleChangeList()
      .then(({ hogeFugaList, hoge, fuga }) => {
        if (hogeFugaList === null) {
	  return
	}
        setHogeFugaList(hogeFugaList)
      })
      .catch((e) => console.error(e))
  }, [setHogeFugaList])

  return (
    <ul>
      {stateHogeFugaList.map((hogeFuga, index) => {
        return <li key={index}>{hogeFuga}</li>
      })}
    </ul>
  )
}

おわりに

変更前
ResponseHeaderBefore
ResponseTimeBefore
変更後
ResponseHeaderAfter
ResponseTimeAfter
上記の解決策を行うことでCacheHitするようになり、レスポンスタイムも速くなりました!
CacheHitさせるための1案ですので、要件によって適宜応用していただければと思います。

参考

https://nextjs.org/docs
https://nextjs.org/docs/app/api-reference/functions/headers
https://nextjs.org/docs/app/api-reference/functions/cookies
https://nextjs.org/docs/app/building-your-application/rendering/server-components#dynamic-functions

株式会社モニクル

Discussion

spectorspector

動的部分はSuspenseすればいいのでは

disk_inuedisk_inue

Suspenceもありますね!
自分のプロダクトを想像しながら書いていたので、ClientComponentではない場合はuseEffect使うよりシンプルですね!
ありがとうございます!