Firebase HostingでNext.jsのISRっぽいことを実現する

## はじめに

先日このような記事を書きました。

https://qiita.com/centerfield/items/349ae9094872168ae86f

タイトルの通りISRはFIrebase Hosting (Cloud Function) では動きませんでした。
理由は以下の説明がとてもわかりやすかったです。

>ISR の本体は、 SSG された HTML をキャッシュし、ユーザのアクセス応じて適切に生成・配信・破棄を行う機能です。
単に IaaS やコンテナにデプロイしただけでは、CDN の設定をすればキャッシュはされるものの、パージが ISR の想定通りには動きません。
コンピューティングリソース内部にキャッシュする(ステートフルにする)場合はその限りではありませんが、 CDN を通さない分のパフォーマンス影響が発生しますし、これは ISR とは異なるものとなります。
lambda や cloud functions ではもはやまともに動かないでしょう。
>
[Next.js のインフラ要件と選択肢](https://blog.euxn.me/xsymsnvwjbkaaaaaaacrug)

ということでISRの実現が難しいことは理解しましたが、諦めきれなかったので、
今回はISRではなく、**ISRっぽい**動きの実現を目指します。


## やっていく

この記事がとても参考になりました。

https://zenn.dev/catnose99/articles/0b601c1f62019b

アプローチとしてはざっくり書くとこんな感じです。
**SSG + ISR　➡︎　SSR + Cache-Control**

サーバーサイドレンダリングした内容をCDNにキャッシュし、
そのキャッシュの保持期間をCache-Controlで指定します。

stale-while-revalidateに対応しているCDNであればISRっぽい挙動を実現できます。

そしてどうやらFirebase Hostingはstale-while-revalidateに対応していることがわかりました。

https://twitter.com/isyumi_net/status/1374890880641892352

おそらくですが、理由はFirebaseのCDNはstale-while-revalidateに対応している[Fastly](https://www.fastly.jp/)が担っているからだと思われます。

>Fastly, Inc., headquartered in the United States, to deliver content and managed services on Firebase Hosting to end-users, via the Fastly content delivery network.
>"米国に本社を置くFastly, Inc.が、Firebaseホスティング上のコンテンツやマネージドサービスを、Fastlyコンテンツデリバリーネットワークを介してエンドユーザーに配信する。"

>[Firebase Subprocessors](https://firebase.google.com/terms/subprocessors)

### さっそくコードを書き換えていく

`getStaticPaths`は不要になるので消し、
`getStaticProps` → `getServerSideProps`に書き換え、`revalidate`も消します。

```index.jsx
// export async function getStaticPaths() {
//   return {
//     paths: [],
//     fallback: true,
//   };
// }

//export async function getStaticProps({ params }) {
export async function getServerSideProps(context) {

//省略

 rreturn {
    props: {
      data,
    },
    // revalidate: 600,
  };
}
```

そして`Cache-Control`を設定します。
参考：[Cache-Control を設定する](https://firebase.google.com/docs/hosting/manage-cache?hl=ja#set_cache-control)
参考：[getServerSideProps (Server-side Rendering)
](https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering)

```index.jsx

export async function getServerSideProps(context) {
  context.res.setHeader("Cache-Control", "public, s-maxage=3600, stale-while-revalidate=7200");

//省略

return {
    props: {
      data,
    },
  };
}
```

`s-maxage`の設定で3600秒 (1時間) はキャッシュを表示し、その後のアクセスでは1度キャッシュを表示した上で裏側でキャッシュの更新をし、その次のアクセスでは新しいキャッシュを表示する。
かつ`stale-while-revalidate`の設定で7200秒経過した時点でキャッシュが破棄され、その次のアクセスでは最新のデータが表示される。

という設定をしました。
細かいCache-controlの説明は[MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Cache-Control)のサイトがわかりやすいです。

### デプロイ

これでCloud Functionsにデプロイすると、無事ISRっぽい動きになりました。

## 注意点：プリフェッチの挙動について

冒頭で示した記事内に記載されていますが、
getServerSidePropsに変更したことによってプリフェッチの挙動が変わります。

>getStaticPropsを使ったときはページのデータまでプリフェッチが行われます。一方でgetServerSidePropsを使ったときはJSファイルのみがプリフェッチされ、データまではプリフェッチされません。
[stale-while-revalidate対応のCDNでISRと同じような挙動を実現する](https://zenn.dev/catnose99/articles/0b601c1f62019b#%E6%B3%A8%E6%84%8F%E7%82%B9)

VercelでISRをしていたときは中のデータまでプリフェッチしてくれましたが、
今回の方法ではデータまでプリフェッチされません。

一般的なコンテンツサイトであればあまり問題にならないかもしれませんが、
設計によっては注意が必要です。

例えばちょっと極端な例ですが、
ナビバーに４つの目次があり、クライアントサイドナビゲーションでページ遷移するとします。
A, B, C それぞれ DB からデータをフェッチしており、
"すべての情報" ページは A, B, C すべてのデータをフェッチしています。
　
<iframe src="https://docs.google.com/presentation/d/e/2PACX-1vRDDR-jsSwvB22s3ggJkEUJxw1GzfVHGUS-IdU3lFK-T_CHF7YOK920rEL6V5LaUZDdkveZ1CTxHk7h/embed?start=false&loop=false&delayms=3000" frameborder="0" width="960" height="569" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe>
　
クライアントサイドナビゲーションとはいえ、ページが分かれているのでそれぞれのページがCDNにキャッシュされることになりますが、
問題は**キャッシュの破棄されるタイミングがバラつくために、ページ間の情報に不整合が出る**ことです。

例えば、 C の DB に情報が追加されました。
その後キャッシュが破棄されたタイミングで "Cの情報" ページが更新されました。
しかし、"すべての情報" ページが古いキャッシュのままだったとします。

するとこのような動きになります。
<iframe src="https://docs.google.com/presentation/d/e/2PACX-1vRfQuz5ydyr7tMT0ZD0vc88qhY2D7ULULiHF9Uqs5q1kezetc07Op5z_xNvuf2fVHl7R-svWr8p8PfB/embed?start=false&loop=false&delayms=3000" frameborder="0" width="960" height="569" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe>
　
今回の例はページの中にナビバーも含めていた極端な例でしたが、この例でもVercelのISRでは中のデータまでプリフェッチすることにより問題なく機能するため、
Vercelから別の場所に移動するときは、設計によってはこのプリフェッチの挙動に違いによって影響がありますよというお話でした。

## さいごに

Vercelに上げる気満々で作ったNext.js製アプリが大人の事情でFirebaseを指定されることになり、戸惑いながら色々調べていましたが、
・[Next.js のインフラ要件と選択肢](https://blog.euxn.me/xsymsnvwjbkaaaaaaacrug)
・[stale-while-revalidate対応のCDNでISRと同じような挙動を実現する](https://zenn.dev/catnose99/articles/0b601c1f62019b#%E6%B3%A8%E6%84%8F%E7%82%B9)
これらの記事のおかげで何とか解決することができました。
（本当はもっと他にもここには挙げきれないぐらいたくさんの記事を参考にしました）

書いてくださった皆さんには本当に感謝しています。ありがとうございました。