💸

Custom Next.js Cache Handler - Vercel以外でのNext.jsキャッシュ活用

2024/02/15に公開

Next.jsアプリケーションのデプロイ先としてVercelはとても利便性が高く、優れたプラットフォームです。一方で、インフラ的な都合やコスト的な都合でSelf-hosting、つまりVercel以外を利用したいケースはそれなりに多いのではないでしょうか。実際、筆者の周りではSelf-hostingでNext.jsを利用しているケースは多く見受けられます。Next.jsはVercel非依存なOSSと銘打ってますが、実際にはNext.jsに必要なインフラ仕様をVercelが隠蔽しているため、Self-hostingでNext.jsを利用するには制約があったり高い理解が求められがちです。昨今、App Routerの登場とその強力なキャッシュ戦略により、Vercel以外でNext.jsを扱うことはより難しくなってきました。

一方で、Self-hosting向けのドキュメントや対応は少しづつですが取り組みがなされています。今回はその一貫で出てきた、Custom Next.js Cache Handlerを利用してNext.jsのキャッシュをRedisに保存する方法について紹介します。

Next.jsのキャッシュ

Next.jsにはいくつかのキャッシュが存在し、App Routerにおいては4種類ものキャッシュがあります。

https://nextjs.org/docs/app/building-your-application/caching

特にリクエストを跨いでサーバー側で共有されるキャッシュとして、Pages RouterではISRのキャッシュ、App Routerではデフォルトで有効なFull Route CacheData Cacheが挙げられます。これらはデフォルトではファイルキャッシュとなっています。

Self-hostingのインフラ構成は通常複数サーバーやサーバーレスであろうことを想定すると、ファイルキャッシュは少々勝手が悪いものと言えます。AWSではEFSなどを用いて複数インスタンス間でファイル共有する手段など考えられますが、CDN側でオリジンシールドを有効にしていないとファイルI/Oの競合状態が発生しうるなど注意すべき点があり、Next.jsとインフラ構成について高い理解が必要となります。

これらの代替策としてキャッシュ永続用のRedisなどを用意することが考えられますが、残念ながらNext.jsにはこれまでキャッシュ永続化をカスタマイズできるオプションなどは存在しませんでした。

Self-hostingサポート

しかしApp Router登場以降、Self-hosting周りのフィードバックが多数あったようで、キャッシュ永続化先をカスタム実装できるCustom Next.js Cache Handlerという機能が追加されました。

https://nextjs.org/docs/app/api-reference/next-config-js/incrementalCacheHandlerPath

これにより、ファイルキャッシュではなくRedisでキャッシュ永続化ができるようになりました。

また、これとほぼ同時にドキュメントの改善も行われ、公式ドキュメントからSelf-hosting時の注意点や実装方法などの情報を従来より容易に得られるようになりました。Self-hostingサポートは大きく進展したと言えると思います。

https://github.com/vercel/next.js/pull/58027

Custom Next.js Cache Handler

さて、Self-hostingでは必須に近いこのCustom Cache Handlerですが、まずこの機能を利用するにはnext.conifg.jscacheMaxMemorySize: 0を指定して、Next.jsのインメモリなキャッシュを無効にする必要があります。

module.exports = {
  cacheMaxMemorySize: 0, // disable default in-memory caching
}

Next.jsにはファイルキャッシュから読み取った値をさらにインメモリに保存する内部キャッシュが存在し、上記はこれを無効にするオプションです。これをしないとせっかくファイルキャッシュをやめてもキャッシュ不整合が発生する余地が生まれてしまうので、指定は必須です。

キャッシュのハンドリング処理自体はインターフェース仕様に基づいて実装することでカスタマイズできます。現在は3つのメソッド(get/set/revalidateTag)を実装することで、キャッシュの読み書きなどの実装が可能です。

https://nextjs.org/docs/app/api-reference/next-config-js/incrementalCacheHandlerPath#api-reference

しかし、これらを自前で実装するのはRedisやキャッシュ周りの知識も必要となり、また大抵の場合似通ってくることから少々面倒な作業です。そのため公式のexamplesでは、neshcaというライブラリを利用した実装例が提供されています。

neshca

neshcaというライブラリは、Next.js Shared Cacheの略です。

https://caching-tools.github.io/next-shared-cache

筆者にはVercelとの直接的な関係は確認できなかったので、おそらく有志による開発だと思われます。これを使うことで、Redisに対するキャッシュの読み書きが簡単に実装できます。

Next.jsの公式examplesであるCustom Cache Handlerの実装例から、以下のような実装でRedisにキャッシュを永続化することが可能です。

// next.config.js
module.exports = {
  cacheHandler: require.resolve('./cache-handler.js'),
  cacheMaxMemorySize: 0, // disable default in-memory caching
}

// cache-handler.js
const { IncrementalCache } = require("@neshca/cache-handler");
const createRedisCache = require("@neshca/cache-handler/redis-stack").default;
const createLruCache = require("@neshca/cache-handler/local-lru").default;
const { createClient } = require("redis");

const client = createClient({
  url: process.env.REDIS_URL ?? "redis://localhost:6379",
});

client.on("error", (error) => {
  console.error("Redis error:", error.message);
});

IncrementalCache.onCreation(async () => {
  // read more about TTL limitations https://caching-tools.github.io/next-shared-cache/configuration/ttl
  function useTtl(maxAge) {
    const evictionAge = maxAge * 1.5;

    return evictionAge;
  }

  let redisCache;

  if (process.env.REDIS_AVAILABLE) {
    await client.connect();

    redisCache = await createRedisCache({
      client,
      useTtl,
    });
  }

  const localCache = createLruCache({
    useTtl,
  });

  return {
    cache: [redisCache, localCache],
    // read more about useFileSystem limitations https://caching-tools.github.io/next-shared-cache/configuration/use-file-system
    useFileSystem: false,
  };
});

module.exports = IncrementalCache

あとは実際のRedisインスタンスをDockerなどで立ててREDIS_URLを指定してあげれば、ローカル環境でもキャッシュをRedisに保存するようになります。筆者としては上記に加え、キャッシュのkeyにgit hashをprefixにする設定を追加したいところです。

const redisCache = await createRedisCache({
  client,
  keyPrefix: process.env.GIT_HASH,
});

まとめ

neshacaやCustom Next.js Cache Handlerはまだ登場したばかりのため、プロダクションでの実運用においてどういう問題が起きるかなどについては筆者には未知数です。一方、これまでSelf-hosting向けのサポートは手薄く感じていたので、こういう機能が出てきたことは大きな進展とも考えています。

実際、これはPaaSのVercelを売るには不利な機能なはずです。それでもこういう機能が出てきたのは、App Routerの普及にはSelf-hostingのサポート需要が無視できないものだったということなのかもしれません。今後Self-hosting向けのNext.jsの機能開発がより進展することを期待したいところです。

Discussion