Next.jsのAPP routerのfetch revalidate をvercelで試したい【vercelで検証する その1】

2023/10/12に公開

実際にrevalidateが機能しているのか、vercel上で試したメモ。

Next.jsのバージョンは13.5.4。

page routerのISRはnext startで確認できるのだけど、APP routerのfetch revalidateは動かなかったので、vercelで試してみただけという話。

APP routerのfetch revalidateについては以下で。

https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#time-based-revalidation

単なる実験のメモ書きなので、他の人には役に立たないけど、自分のための備忘録。

まあ多分だいたい想定通りに動いてるんじゃない?という感じ。

いろいろ試した結果、複数のブラウザを使ってアクセスしていて、ブラウザキャッシュが残っている場合に想定外の挙動が起きることが多い、ということはわかった。

結論

  • だいたい想定通りに動く

  • ブラウザキャッシュの影響で、X-Vercel-CacheのHITになるはずのものがSTALEになることがある

  • 例外動作はブラウザキャッシュのことが多い

    • すでに再検証されているのにX-Vercel-Cache:STALEになる
    • サーバーレスファンクションでfetchを入れた関数のログが出る
    • ログが出てもAPIをコールしていない模様
  • APIコールにたいする従量課金サービスを使う場合は、細かい検証をしたほうが良さそうではある

  • サーバーレスファンクションが動くのでvercelのリソースの確認のほうが必要かも

だいたいちゃんと動くけど、ブラウザキャッシュが絡むと変な挙動をすることがある。再検証が動いた後なのに、再度X-Vercel-Cache:STALEにり、サーバーレスファンクションが動いてしまうということがある。

ただ、サーバーレスファンクションは動くけど、実際にAPIにリクエストは送っていなかった。

個人レベルで使う分には大して気にしなくても良さそうだけど、APIが有料で、コールする回数も決まっていて、なんてサービスを使う場合には、ちゃんと検証したほうが良さそうだなとは思う。

あと、どちらかというとvercelのリソースのほうが気になるところ。

以下はその結論に至る過程でいろいろやったことや、その時に思ったことを書いただけなので、読む価値はそんなにない。

外部のAPIをfetch revalidateで試してみる

こんな感じのfetchする関数を作る。

APIのURLは仮。

import ja from "date-fns/locale/ja";
import { format } from "date-fns";

export async function getTestISR() {
  const now = new Date(
    Date.now() + (new Date().getTimezoneOffset() + 9 * 60) * 60 * 1000,
  );

  const nowJa = format(now, "yyyy年M月d日(E) HH:mm:ss", { locale: ja });

  const diff = 3600; //1時間
  const code = "test"; //想定機能はコードが可変なため変数で

  const baseUrl = "https://www.isr-test-ex.com/";//仮のURL
  const pathname = code;
  const fetchUrl = new URL(pathname, baseUrl);

  const res = await fetch(fetchUrl, { next: { revalidate: diff } });
  console.log(`リクエスト時間 ${nowJa}`);
  console.log(`getTestISRを${code}でリクエスト`);
  console.log(`ステータス ${res.status}`);
  console.log(`再検証まで-------`);
  console.log(hour);

  // Recommendation: handle errors
  if (!res.ok) {
    // This will activate the closest `error.js` Error Boundary
    throw new Error("Failed to fetch data");
  }

  return res.json();
}

さっきの関数をこんな感じのコンポーネントで呼び出してみる。

import { getTestISR } from "../_lib/getTestISR";

export default async function Testisr() {
  const data = await getTestISR();
  const apiData = data.name
  return (
    <>
      <div className="flex h-screen items-center justify-center">
        <div className="text-[30px]">
          <div className="mb-[50px]">test isr</div>
          <div className="text-center">
            <a href="/testisr">apiData:{apiData}</a>
          </div>
        </div>
      </div>
    </>
  );
}

次にvercelでビルド。

vercelでビルドすると、ビルド時に二回関数が呼ばれてデータができるのが、Logから確認できる。

リクエスト時間 Thu Oct 11 2023 22:30:34 GMT+0000 (Coordinated Universal Time)
getTestISRをtestでリクエスト
レスポンスコード 200
再検証まで-------
60
リクエスト時間 Thu Oct 11 2023 22:30:35 GMT+0000 (Coordinated Universal Time)
getTestISRをtestでリクエスト
レスポンスコード 200
再検証まで-------
60

2回呼ばれるのは仕様らしい。以下に書いてある。関数は2回呼ばれるけど、APIへのアクセスは一回だけ。これはローカル環境でビルドして試したらそういう動きだった。

https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#example

1時間、適当にいろいろ更新とかしても、fetchが呼ばれたLogは出ず、一時間後が以下。

リクエスト時間 Thu Oct 11 2023 23:33:27 GMT+0000 (Coordinated Universal Time)
getTestISRをtestでリクエスト
レスポンスコード 200
再検証まで-------
60
リクエスト時間 Thu Oct 11 2023 23:33:27 GMT+0000 (Coordinated Universal Time)
getTestISRをtestでリクエスト
レスポンスコード 200
再検証まで-------
60

再検証後の動作も最初は通常通りだったが、その後、様々なケースを試すと、想定外の動きをするパターンがあることに気づく。

あとからわかったことだけど、だいたいブラウザキャッシュが原因のことが多かった。

X-Vercel-CacheがSTALEになって、裏でfetchが動いてキャッシュが更新されているのに、ブラウザキャッシュが残っている別のブラウザでアクセスすると、X-Vercel-CacheがSTALEになってしまう。

もしかしたら他にも原因はあるかもだけど、切り分けができて確実に再現できたのは、ブラウザキャッシュくらいだった。

vercelにテスト用APIを上げてfetch revalidateで試してみる

Next.jsのrouter.tsを使ってテスト用のAPIを作る。

アクセスした日付と時間を返すだけの簡単なもの。

二度目のX-Vercel-Cache:STALEの時に、APIが叩かれていれば、そのログが残りつつ返ってくる日付も最新の時間になるはずで、それが可視化されるとわかりやすいのでは、というのが日付と時刻を返すAPIにした理由。

Next.jsのAPI側「router.ts」の仕様で1つ注意点。開発環境だとキャッシュは無視されるので気にならないけど、デプロイするとキャッシュが残るので、export const dynamic = "force-dynamic";を入れてキャッシュしないようにする。そうしないと、いつアクセスしてもビルドしたときの日付しか返ってこない。

import ja from "date-fns/locale/ja";
import { format } from "date-fns";

import { NextResponse } from "next/server";

export const dynamic = "force-dynamic";

export async function GET() {
  const now = new Date(
    Date.now() + (new Date().getTimezoneOffset() + 9 * 60) * 60 * 1000,
  );

  const nowJa = format(now, "yyyy年M月d日(E) HH:mm:ss", { locale: ja });

  console.log(">>> Called api アクセスした日付を返すAPI");
  console.log(nowJa);
  return NextResponse.json({ data: nowJa });
}

どちらもvercelに上げて試してみた。

初回アクセスが以下。

テストサイトのログ
リクエスト時間 2023年10月12日(木) 19:02:20
getTestDateをtest-apiでリクエスト
レスポンスコード 200
再検証まで-------
30
リクエスト時間 2023年10月12日(木) 19:02:20
getTestDateをtest-apiでリクエスト
レスポンスコード 200
再検証まで-------
30
テストAPIのログ
>>> Called api アクセスした日付を返すAPI
2023年10月12日(木) 19:02:20

再検証後が以下。

テストサイトのログ
リクエスト時間 2023年10月12日(木) 19:33:14
getTestDateをtest-apiでリクエスト
レスポンスコード 200
再検証まで-------
30
リクエスト時間 2023年10月12日(木) 19:33:16
getTestDateをtest-apiでリクエスト
レスポンスコード 200
再検証まで-------
30
テストAPIのログ
>>> Called api アクセスした日付を返すAPI
2023年10月12日(木) 19:33:16

想定通り動いた。

あと、ブラウザキャッシュ時の問題、二度目のX-Vercel-Cache:STALEの時にはサーバーレスファンクションは動いているけど、APIは叩かずにキャッシュが返っていることが確認できた。

ブラウザキャッシュが原因とわかるまでは再現できなくて大変だった。

補足 X-Vercel-Cacheについて

vercelのlogやブラウザのdevtoolsで見られるレスポンスヘッダーの「X-Vercel-Cache」を確認すると、キャッシュの挙動がわかる。

https://vercel.com/docs/edge-network/headers#x-vercel-cache

キャッシュにアクセスできたらHIT、古いキャッシュにアクセスして裏でキャッシュが更新されているのがSTALE、オリジンサーバーからデータがくるのがPRERENDER、ほかにもあるけどrevalidateでよく出てくるのはこんな感じ。

初回ビルド後はPRERENDERが出てて、その後はHITが出る。再検証の時間が来て最初のアクセスではSTALEがきて、次のアクセスはHITになる。

普通にアクセスしているだけなら、この一連の挙動はほとんど変わらないので、fetch revalidateが想定通りに動いているのがわかる。

複数のブラウザをまたいでも、同じように動作するのを確認したのだけど、100%想定通りに動くのはdevtoolsでキャッシュを無効化した場合のみ。

キャッシュを無効化せずにいろんなブラウザで挙動を確認するとたまに想定通りではなくなる、というのが何度も試してみてなんとなくわかってきたこと。

もしかしたら他の原因もあるかもしれないけど。

Next.jsの中身を見てみればすぐにわかるかもしれないけど、今回は表面の事象だけを追った形。

再検証後複数fetchログ出力の原因の一つは多分ブラウザキャッシュ

再検証後に何度もfetchのログが出る1つの理由は、ブラウザキャッシュっぽい。複数のブラウザやユーザーを変えたchromeやシークレットモードなど、いろんなブラウザでアクセスしそのキャッシュが残っている状態で、他のブラウザで再検証が走ったあとに各種ブラウザで見ていくと、再度再検証が走ってSTALEを返すことが多い。

例えば、chromeで再検証時間後にアクセスしてSTALEが出て、その後に再度アクセスするとHITになるのだけど、キャッシュを無効化していないfirefoxでアクセスするとSTALEが出る、なんて動き。

この時、サーバーレスファンクションが動いてfetchが動いている関数のログが作動して、vercelのログ管理画面でその動きが確認できる。

この動きはfirefoxとchromeのパターンだけではなくて、chromeで違うユーザーを使ってもなる。一度該当ページを開いてブラウザキャッシュが存在する状態を作り、他のブラウザで再検証が走った後に、ブラウザキャッシュが残っているブラウザでアクセスするとSTALEになる、というのが、判明した原因の一つ。

キャッシュを無効化していれば、firefoxでもchromeでも、どこかで一度STALEが出ていれば、他のブラウザを見てもHITが出る。これは100%動作した。

まとめ

最初に書いた結論通り、自分が試した範囲内ではだけど、ブラウザキャッシュ以外では想定通りに動いているのが確認できた。

また、ブラウザキャッシュで再度STALEになっても、最初に再検証が入った時のキャッシュがデータとして送られてきていて、再度APIを叩いていない。

そのため、API側のコールについては影響なさそうではある。もし従量課金のAPIを使う場合は、本当にAPIへのリクエストが出ていないか、実際のAPIと実際の本番環境で確認したほうが良さそうだけど。

1つ気になるのは、サーバーレスファンクションは動いているのでvercelのリソースは使っているということ。そのため、vercelのリソース使用量を気にする必要があるのかもしれない。fetchの数にもよるかもだけど、個人で使う分には無視して良さそうな気がする。

できればpageルーターのときみたいに、next startで確認できるようにして欲しいなと思った。

cron-jobみたいに毎日0時にrevalidateで再検証する、みたいなことも、vercel上で試せたので、本番環境に上げてみて良かったとは思うけど、できればローカルでも動作確認したい。

Discussion