😽

Next.jsのキャッシュ機能を活用して高速かつ古いデータを避けるウェブサイトの作り方

2024/10/09に公開

初めに

最近、NextJSの最新構築方で日本語学習の個人アプリを作りました。
https://japanese-memory-rsc.vercel.app
(詳細はこの記事に書いてあります:https://zenn.dev/chenbj/articles/555a42958b5a3e)

ただ、最初のバージョンの性能はやや劣っており、シンプルな画面だけど初めに移動する際に1〜2秒かかることがあります。

それを最適化ために、NextJSのキャッシュ原理をきちんと研究して、研究結果を自分のアプリに応用しました。現在、このウェブサイトのすべてのルートは、初めてアクセスしても瞬時に入り、古いデータにアクセスすることを避けることができます。

次は最適化方を紹介します、原理をよく理解したい場合はこの記事を先に読んでください。もし実践を優先したいと思っており、わからないことがある時に原理を学ぶのであれば、この記事を引き続き読んでください。

なんで初回だけが遅い

要するに、キャッシュがないため、RSCを実行しないとリンダリングに必要なRSC PayloadやRSC HTMLを取得できません。

初回もキャッシュを利用したい場合は、プリレンダリングとプリフェッチの2つの手段があります。

プリレンダリング

プリレンダリング対応するNextJSキャッシュ手段はFull Route Cacheです。
プリレンダリングは、ビルドのとき、RSCを実行し、RSC PayloadやRSC HTMLを取得して保存します。こうすると、初回アクセス時もリンダリングに必要なリソースは全て用意済みです。

プリレンダリングはデフォルト動作なんですが、動的なAPIを利用してる場合プリレンダリングは無効になります。

自分のアプリケーションを例に取ると、すべてのページはまず現在ログインしているユーザーのIDを読み込み、その後ユーザーIDに基づいてDBをクエリする典型的なダイナミックページです。ビルド時にはもちろんログインしたユーザーのIDを取得できないし、事前レンダリングが行えません。

プリフェッチ

プリフェッチ対応するNextJSキャッシュ手段はClient-side Route Cacheです。
プリフェッチは、クライアントが初回アクセスした後に、ルートのRSC Payloadをまだアクセスしていないものでも事前にリクエストすることです。

この動作はデフォルトではないです、どのルートがプリフェッチされる必要があるかを手動で指定する必要があります。

src/app/client-layout.tsx
<Link className="w-full" prefetch href="/word-cards">単語帳</Link>

Linkタグにレンダリングされると、そのルートへのRSCペイロード要求がブラウザーから発信されます。
そして、Linkタグはレイアウトに書かれており、どんなページでも存在するため、どこから入ってもそのルートのRSCペイロードをリクエストします。
このようにして、実際にそのルートに入るまで全てのリソースがキャッシュされているため、瞬時に入ることができます。

ただし、システムに多くのルートがある場合、すべてを事前読み込みするのではなく、router.prefetchを使用して細かい事前読み込みを行うべきです。

旧データの防止方法

prefetchが行われた後、実際にアクセスするまでデータが変更された場合、キャッシュを無効にする必要があります。

その方法は簡単で、revalidatePath("/word-cards")という1行で完了します。ただ、この行のコードはServer Actionsに書く必要があります。
個人的な意見ですが、DBの更新はServer Actionsで行うことをお勧めします。特定のルートが無効になる必要がある場合は、この行のコードを引き続き記述してください。

src/components/word-adder/server-actions/index.ts
"use server"
import { auth } from "@/auth";
import { prisma } from "@/prisma";
import { revalidatePath } from 'next/cache';

export async function insertWordCard(word: string, meaning: string, memoCardId: string) {
    const session = await auth();
    let newWordCard = {}

    if (session?.userId) {
        newWordCard = await prisma.word_card.create({
            data: {
                word: word,
                meaning: meaning,
                create_time: new Date(),
                user_id: session?.userId,
                memo_card_id: memoCardId,
            },
        });
        revalidatePath("/word-cards")
    }

    return JSON.stringify(newWordCard);
}

まとめ

完全なコードは、このリポジトリでご覧いただけます。
https://github.com/chenbj5515/japanese-memory-rsc

実戦体験としては、RSC時代のNextJSは開発体験も性能面もめちゃくちゃ魅力的です。prefetchだけを使用するだけで、著しい最適化効果が得られます。ただ、NextJSのさまざまなキャッシュメカニズムを完全に理解することは簡単ではありません、この記事では筆者の理解を紹介しています。

https://zenn.dev/chenbj/articles/490c14355c74f2

Discussion