Next.jsのApp Router Cacheにご注意

2024年04月29日

App Routerを利用する場合はClient Route Cacheに注意

これからNext.jsでWebアプリ開発を始める場合、旧式のPage RouterではなくApp Routerを採用すると思いますが、App RouterのClient Route Cacheの振る舞いと制約をよくよく理解した上で利用した方が良さそうです。

Client Route Cacheの仕様はざっくり以下となります(詳細は公式docを参照)

  • Client Route Cache はブラウザのメモリ上にルート毎(URL毎)に保持されるキャッシュ機構である
  • Route Cacheが有効なルートへの再アクセス時にサーバへの通信は走らない
  • Route Cacheの有効期間は<Link>タグのprefetch未指定時30秒prefetch={true}指定で5分
  • Route Cacheの適用条件や有効期間をルート毎にカスタムすることは 「できない」!!!
  • 明示的にrouter.refresh() することで全ルートのRoute Cacheをクリアすることは可能
  • 利用者がブラウザリロードすることでもクリアすることができる

※2024/4/3にリリースされたv14.2.0-canary.53でキャッシュ期間を変更するexperimentalな機能が追加されましたが、ルート毎に細かくキャッシュ条件を設定できるものではありません。

実務上大きい制約になりうる「Route Cacheの適用条件や有効期間をルート毎にカスタムすることができない」問題

複数のユーザーが利用する動的コンテンツを生成するWebアプリにおいて、この制約は結構大きな障壁になると考えています。 例えば、メッセージ機能を実装し、メッセージBOX画面と相手とのメッセージやり取り画面があるとします。

  1. AさんとBさんが同時にサービスを利用している
  2. BさんがAさんにメッセージを送り、別画面に遷移する
  3. AさんがBさんにメッセージを返信する
  4. Bさんが再度メッセージやり取り画面を開く

Bさん視点で見たときにステップ2とステップ4の間に5分待たないとキャッシュがクリアされず、ステップ3でAさんから送られてきたメッセージが画面に描画されません。

App RouterとServer Componentの導入でNext.jsはよりサーバレンダリングを推奨するようになりましたが、このClient Route Cacheはサーバレンダリングとの相性が最悪と言わざるを得ません。

試行錯誤したところClient Route Cacheを回避する正攻法なやり方はなく、

  • 動的コンテンツをCSRする(Next.jsを使っている意味が・・・)
  • Linkコンポーネントのwrapperを作り、クリック時に強引にroute refreshする。キャッシュはクリアされるので最新コンテンツが常に取得可能だが、リンククリック時にサーバに同一リクエストを二回送ってしまう

これくらいしか回避策がありませんでした。 Linkコンポーネントのキャッシュ対策としたDynamicLinkコンポーネントの独自実装例は以下です。

"use client"

import { useRouter } from "next/navigation";
import Link from "next/link";
import { UrlObject } from "url";
import { MouseEvent} from "react";

type PropsType = {
    children: React.ReactNode,
    href: string | UrlObject,
    as?: string | UrlObject;
    replace?: boolean;
    scroll?: boolean;
    prefetch?: boolean;
    className?: string;
    onClick?: (e: MouseEvent<HTMLElement>) => void;
}

export default function DynamicLink({children, href, as, replace, scroll, prefetch, className, onClick}: PropsType) {
    const router = useRouter();

    const refreshClick = (e: MouseEvent<HTMLElement>) => {
        router.refresh();
    }

    return (
        <div
            onClick={refreshClick}
            className="flex p-0"
        >
            <Link
                href={href}
                as={as}
                replace={replace}
                scroll={scroll}
                prefetch={prefetch}
                className={`${className} flex-1`}
                onClick={onClick}
            >
                {children}
            </Link>
        </div>
    );
};

Next.jsのdevチームの対応状況と個人的な感想

  • この問題については昨年App RouterがStable化した際に議論になっています。
  • 開発チームも課題として認識しているようで、2024年4月に一旦experimental機能でClient Route Cacheの有効期間をコントロールする機能をリリースしました。
  • (個人的には)このexperimental機能はゴールではないと思っており、App Routerの利点であるキャッシュによる効率化の恩恵を受けられなくなる両刃の設定でもあります(ルート毎にキャッシュ期間を設定できないから)
  • 趣味や個人でやるサービスならスルーできるけど、業務利用ではこの問題が解消しない限りNext.jsのApp Routerは選択肢にできない

まとめる

Next.jsは素晴らしいフレームワークで個人的に大好きです。App RouterもPage Routerと比べてCSR/SSRがよりシームレスに統合されているので、進化の方向性としては革新的だなと思ってますし、まだApp Routerが導入されて1年も経ってないので今回ご紹介したキャッシュ問題もいずれ対応していただけるのではないかと期待しています。

ただ、この問題は実際のサービス開発においてはかなりマイナスインパクトが大きいと思うので、2024年4月時点でNext.jsを利用する際には注意した方が良いと思います。


Profile picture

Written by なまちゃ Web系エンジニアPython好き。バックエンド/フロントエンド問わずマルチな方面でエンジニアリングしています。