Nextjsはユーザー固有データのキャッシュしてくれないよってお話 - キャッシュの仕組みの概要を添えて
背景
どうも。NextjsでWebアプリを開発しているけーじと言います。もともとはReactを使ってアプリ開発をしていたのですが、キャッシュを使えばアプリの動作が速くなって最強じゃんと思ってNextjsに乗り換えました。
しかし、乗り換えたは良いものの...
あんま早くなってないじゃん!
確かに早くなってはいるのですが、わざわざNextjsに乗り換えたうまみが足りないような気がします。ただ、後ほど解説しますがこれはNextjsのキャッシュに関して私が誤認していたことが原因だったのでこの記事ではその辺をお話していこうと思います。
この記事の対象者
この記事では、Nextjsでユーザー固有のデータを扱っていて、以下の状況に陥っている人を対象にしています。
- いまいちキャッシュの恩恵を受けられている気がしない
- キャッシュって難しいからまずは雰囲気だけつかみたい
あんまりキャッシュの詳しい話や専門用語には深入りせず伝わりやすく書いてあるのでぜひおやつでも食べながら読んでいってください。
前提とする設計(ユーザーIDの使い方)
この記事のユーザー固有のデータ(以降ユーザーデータ)とは、アプリを使用しているユーザーidをもとに取得するデータのことを指しています。
Todoアプリでいうところの各Todoのことであって、掲示板アプリなどの全ユーザーが取得できる記事とかのことではないことにご注意ください。あっちは普通にキャッシュの恩恵受けられます。
したがって、今回お話しするユーザーデータは以下のように取得される想定であると考えてください。
- ユーザーがログインしてデータ取得に使うIDがCookieに格納される
- クライアント:ユーザーが自分のデータをリクエストする
- サーバー :CookieからID情報を抜き出し
- サーバー :ID情報に紐づいたデータのみをデータベースから持ってくる
- サーバー :持ってきたデータをユーザーに返却
- クライアント:返却されたデータを画面に表示(やったね!)
多少違いはあるものの、Cookieにユーザーid情報を格納してそのidをもとにデータ取得するぜっていう設計は大体同じです。間違えてもリクエストボディにユーザーid入れてユーザーデータを取得しようなんて思わないでくださいね。めっちゃ危険です。私はそんなことをしてました反省反省。その辺のセキュリティの話はまた分かりやすく記事にしてここにリンクを貼るので楽しみに待っていてください。
今回の肝はこのCookieでユーザーidを管理するというところです。これが原因でNextjsはキャッシュせずアプリもその恩恵を受けられないのです。その辺の話を次にしようと思います。
キャッシュしてくれない原因
さて、ここから本題です。まずはユーザーデータがキャッシュされない原因をお伝えします。とはいっても原因は単純です。実は、Nextjsの仕様で以下のようなリクエストはキャッシュされないよというのが原因です。
- リクエストヘッダーにAuthrizationがセットされている
- リクエストヘッダーにCookieがセットされている
つまり上で説明したようなよくある設計でユーザーを識別してデータを引っ張ることはできないというわけです。
ではなぜこんなことになっているのでしょうか?次章ではその理由を説明していきます。
なぜリクエストヘッダーにユーザー情報が載ってるとキャッシュしないのか
先ほど説明した通り、NextjsではリクエストヘッダーにAuthrizationまたはCookieが載っているとキャッシュしてくれません。では、Nextjs側はなぜこんな設計にしたのでしょうか?結論を簡単に述べると、Nextjsはリクエストのメソッド・URL・ボディをキャッシュの識別キーに使ってるからだよというのが理由です。この説明で分かった人はこの章は読み飛ばしてもらっても構いません。これを皆さん居理解してもらうために、この章では以下の内容について話していきます。
- Nextjsのキャッシュに関する前提(キャッシュの種別・キャッシュの識別方法)
- リクエストヘッダーをキャッシュの識別キーに使わない理由
Nextjsのキャッシュに関する前提(キャッシュの種別・キャッシュの識別方法)
まず、Nextjsのキャッシュの種別について軽く触れていきます。そもそもキャッシュは、どこにデータを取っておくかによって以下の2つに分けられます。
- データを自分の手元のパソコンにとっておく(クライアントキャッシュ)
- webアプリを動かしているサーバーにデータを取っておく (サーバーキャッシュ)
そして、Nextjsが担当してくれるのはサーバーキャッシュの方になります。
次に、Nextjsがキャッシュの有無を判断する方法について説明します。キャッシュは、大まかに以下の手順で利用されます。
- ユーザーがデータAが欲しいと
GET /data-a
にリクエストを投げる - サーバー側でデータAのキャッシュがないか探す
- キャッシュがなかったら渋々データベースからデータを取ってくる
- ユーザーに返す
ここで疑問です。手順2でどうやってキャッシュの有無を確認するのでしょうか?答えはリクエストのメソッド・URL・ボディをもとに生成されるキーを手掛かりにするです。細かい設計の話まではわかりませんが、大まかに以下の通りです。(実際にこんな感じのキーになっているわけではないのであくまでイメージとして)
method: GET, url: /data-a, body: {foo: "bar"}
--> key: "GET_/data-a_{foo: 'bar'}"
これなら同じリクエストメソッド・URL・ボディの組み合わせが来た際に有無を確認できます。
ここまでをまとめます。Nextjsのキャッシュに関して以下のような前提を覚えていただければ次の説明がすんなり理解できるはずです。
- Nextjsはサーバーキャッシュをメインに使っている
- Nextjsはキャッシュの識別にリクエストメソッド・URL・ボディを使用している
リクエストヘッダーをキャッシュの識別キーに使わない理由
ここまで読んで、リクエストヘッダーもキャッシュ識別キーに入れちゃえばいいじゃんと思ったそこのあなた。ご明察です。私もそう思いました。でも思い出してみましょう。Nextjsがメインで担当するのはサーバーキャッシュです。つまりリクエストヘッダーもキャッシュ識別キーに含めた場合、サーバーがキャッシュに割かなければならないデータ容量がとんでもないことになります。
ユーザーが多ければ多いほどキャッシュが増大になるのはもちろん、ネットワーク環境によってもリクエストヘッダーの中身が変わるみたいなので想像を凌駕していますね。そりゃあ全部キャッシュしたらいいかもしれないけどそんなことしたらサーバーパンクしちゃうよねって話です。
ちなみにキャッシュを無理やりしたらどうなるの?
以上の話からお察しの方もいるかもしれませんが、ユーザーデータを無理やりキャッシュしたらどうなるか考えてみましょう。fetchメソッドではオプションを設定することでキャッシュするかしないかをいじることができます。これを利用して以下のような状況を考えます。
-
ユーザーid: 1のA君
がタスク一覧データが欲しいとGET /tasks
のAPIを叩く -
ユーザーid: 2のB君
もタスク一覧データが欲しいとGET /tasks
のAPIを叩く
この時サーバーではリクエストヘッダー内部のクッキーに保存されているユーザーid情報からそれぞれのidを抽出し、そのidに紐づいた各々のタスク一覧データを返却します。
しかし、先ほど述べたようにキャッシュするように設定した場合はどうでしょう。
A君はちゃんとデータを取得できるかもしれません。ただ、B君では以下のようなことが起こります。
なんかA君のタスク一覧データが出てきた!!
これは、以下のような手順でAPIの処理が走ったことによります。
A君フェーズ
- A君のリクエストをAPIが受け取りキャッシュのキーを生成する("GET_/tasks_{}")
- 生成したキーと一致するキャッシュがないか探す
- キャッシュがなさそうなのでデータベースからタスク一覧データを取ってくる
- とってきたデータをキャッシュに保存する。この時の識別キーはさっき生成した"GET_/tasks_{}"
- A君にデータを返す
B君フェーズ
- B君のリクエストをAPIが受け取りキャッシュのキーを生成する("GET_/tasks_{}")
- 生成したキーと一致するキャッシュがないか探す
- キャッシュ発見!(A君の時に発行したものと同じキー)
- 保存されたキャッシュをB君にデータを返す(A君のものだけど)
このように、リクエストヘッダーがA君とB君を識別する手がかりなのに、キャッシュ識別キーにはその情報が含まれていないため2人のキャッシュ識別キーが重複してしまい今回の事件が起こります。タスク一覧データならまだしも、これが機密情報だったら怖いですよね。
Nextjsがユーザーデータをキャッシュしない原因まとめ
というわけで、Nextjsがユーザーデータをキャッシュしない原因は、以下のようになります。
- キャッシュの識別キーにリクエストヘッダーが含まれていない
- Nextjsの担当はサーバーキャッシュなので、リクエストヘッダーまで含めるとサーバーがパンクする
Discussion