🙆‍♀️

Firebase CLIのNext.jsデプロイ対応について調べる

2022/09/02に公開

Firebase HostingがNext.jsのデプロイに対応した[1] と聞きつけ、Next.jsビルドツール好き[2] [3] なので様子を見てきました。

https://github.com/FirebaseExtended/firebase-framework-tools

のリポジトリを中心に調べてみます。

Firebase CLI framework-awareness とは

フレームワークサポートを付与するためのFirebase CLI のアドオン。

Firebaseプロジェクトの構成に応じて、Google Cloudのリソースを構築する。

現在Next, Nuxt2/3, AngularをサポートしていてCloud Functionsにこれからのフレームワーク機能をサポートするエンドポイントを自動でデプロイしてくれる。

内部アーキテクチャ

  1. next export で .next/ ディレクトリができる
  2. firebase-frameworks.build() がプロジェクト構造を解析してフレームワークを検知
  3. firebase-frameworks/frameworks/next.js/build() でCloud Functionsへデプロイするアーティファクトに固める
  4. .firebase/ 以下に吐き出して firebase deploy でGoogle Cloud上に反映する

Getting Started

アドオン機能を有効にする

$ npx create-next-app@latest next-firebase-hosting-swr -e ssr-caching
$ cd next-firebase-hosting-swr
$ firebase init
$ yarn add -D firebase-tools firebase-frameworks
$ npx firebase --open-sesame frameworkawareness

Enabling preview feature frameworkawareness...
Preview feature enabled!

next.config.js

firebase-frameworksのビルドでフレームワークのタイプを検知してもらうためにプロジェクトにnext.config.jsが存在する必要がある(後述)。

テストコードにあった以下をコピーした

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
}

module.exports = nextConfig

ローカルで起動

firebaseコマンドを実行すると内部でnext export が実行されディレクトリが解析され ssrnextfirebasehostingswr という名前のFunctionに詰め込んでHostingのバックエンドに割り当てるところまで全部やってくれる。

$ npx firebase serve

i  hosting[next-firebase-hosting-swr]: Serving hosting files from: .firebase/next-firebase-hosting-swr/hosting
✔  hosting[next-firebase-hosting-swr]: Local server: http://localhost:5000
✔  functions: Loaded functions definitions from source: ssr.
✔  functions[us-central1-ssrnextfirebasehostingswr]: http function initialized (http://localhost:5001/next-firebase-hosting-swr/us-central1/ssrnextfirebasehostingswr).

リソースの実体は .firebase/ 以下にあり、firebase-cliのbuildフックを通してfirebase-frameworksのビルドスクリプトが生成している。

$ find .firebase/next-firebase-hosting-swr -depth 2

.firebase/next-firebase-hosting-swr/hosting/_next
.firebase/next-firebase-hosting-swr/hosting/favicon.ico
.firebase/next-firebase-hosting-swr/hosting/500.html
.firebase/next-firebase-hosting-swr/hosting/404.html
.firebase/next-firebase-hosting-swr/hosting/vercel.svg
.firebase/next-firebase-hosting-swr/functions/server.js
.firebase/next-firebase-hosting-swr/functions/next.config.js
.firebase/next-firebase-hosting-swr/functions/node_modules
.firebase/next-firebase-hosting-swr/functions/.next
.firebase/next-firebase-hosting-swr/functions/public
.firebase/next-firebase-hosting-swr/functions/package-lock.json
.firebase/next-firebase-hosting-swr/functions/package.json
.firebase/next-firebase-hosting-swr/functions/.env
.firebase/next-firebase-hosting-swr/functions/settings.js
.firebase/next-firebase-hosting-swr/functions/functions.yaml

デプロイ

$ npx firebase deploy

ここでデプロイされるリソースは通称gcfv2というCloud Functionのバージョン2の実行環境で内部的にはCloud Runが動いてる。

https://cloud.google.com/functions/docs/concepts/version-comparison

$ gcloud run services list

SERVICE                    REGION
ssrnextfirebasehostingswr  us-central1

$ gcloud functions list

Listed 0 items.
# Runじゃん

その他のCloud Run設定

stale-while-revalidateの挙動

READMEにSWR/E: ❌ と記載されているが、たぶんNext.js機能とのインテグレーションのことで、SWR自体は前から使えていた。

https://next-firebase-hosting-swr.web.app/

今回デプロイしたサイトではサンプルどうりにcache-controlヘッダを

cache-control: public, s-maxage=10, stale-while-revalidate=59

とした。

https://github.com/vercel/next.js/blob/abcf991d11131fe45298f57d3f98e5e23b7dfb3f/examples/ssr-caching/pages/index.js#L1-L31

ブラウザから連続してアクセスして x-cache: HIT かつ コンテンツが更新されたので意図どうりに動作していると判断した。

応答速度

Cloud Loggingから各リクエスト結果を見てみる。

  • コールドスタートで 1-2s
  • ウォーム時は 10-20ms
  • CACHE HIT時はログが残らない
    • Firebase Hosting(Fastly)のレイヤーまでしか到達しないので

となっていた。

コールドスタートは最小インスタンス数を1以上にするとか改善方法がありそう。

https://cloud.google.com/run/docs/configuring/min-instances

APPENDIX

FirebaseAwareServer

https://github.com/FirebaseExtended/firebase-framework-tools/blob/06a731d726693cb6cab6a574fabed36af9541aae/src/server/firebase-aware.ts#L86

FirebaseAwareServerという便利機能がfirebase-frameworksの中にあって、プロジェクト内で @firebase/app をimportしているとサーバーミドルウェアのレイヤーで組込まれる。

これが有効になるとSSRのrequestオブジェクトに

  • req.firebaseApp
  • req.currentUser

を注入してくれて、firebase.jsの初期化(initializeApp)やFirebase Authの認証処理(session)を自動で行ってくれる。

フレームワークのタイプの検知

dynamicImport() 内でプロジェクト内にあるファイルによって検知される。なのでnext.config.jsなしのプロジェクトで試したら検知してくれなかった。

nuxt3はソースコードはあるがたぶんまだ検知されない(2になる)。

https://github.com/FirebaseExtended/firebase-framework-tools/blob/066b6c877fd4da71a96d51944338a4d99dc7fe07/src/frameworks/index.ts#L22-L32

ISRするには

Firebase Hostingはstale-while-revalidateに対応しているので非同期なキャッシュコントロールという意味では一部実現できています

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

ただstale-while-revalidateはブラウザが持つ機能なので、Safariが未対応だったりするので [4] ISRのようなサーバー側のシステムが作れると良さそうです。

Serverless Next.js Component のISR実装を読み解くと同じ方式にすることを考えると、以下のようなパイプラインを構築したいです。

  1. リクエストをトリガーにしてCloud RunからCloud Tasksの非同期処理をenqueueする
  2. Task内で該当のURLのキャッシュをパージする
  3. CDNのキャッシュを更新する
  4. 次のリクエストでレスポンスがキャッシュから返される

これはメルカリ Shopsで実現されているFastlyCache MSに近いと思います。

https://engineering.mercari.com/blog/entry/20210823-a57631d32e/

ただFirebase Hostingではキャッシュをパージする手段はデプロイぐらいしか用意されておらずCDNレイヤーの制御はできません。

——とずっと思っていたのですが、メーリングリストでGoogleの人がPURGEリクエストに実は対応していると発言していました。

Something you can do that is a little bit of an undocumented API (I can't promise it will be around forever but there's no plan to change it) is to issue a request to a specific URL with a PURGE method, e.g.

curl -X PURGE https://my.firebasehosting.site.com/path/to/content

This will purge the CDN cache for that URL, allowing you to potentially do some of the things you're looking for. See my I/O talk for an example of this.

https://groups.google.com/g/firebase-talk/c/_q9qM82QV6U/m/Xsy1OP6BFQAJ

$ curl -X PURGE https://next-firebase-hosting-swr.web.app/

{ "status": "ok", "id": "17920-1662030740-342314" }

と確認してみたらたしかにパージできていました。ということで非同期のパージはできそう。

ただその後に各地のネットワークのキャッシュを更新する方法は思いつかないので、ISRと同等の機能を作ることはできませんでした(Fastly力の高い人に教えてもらいたい)。

キャッシュの生存時間を調べたい

Fastly-Debugヘッダをつける

curl https://next-firebase-hosting-swr.web.app/ -svo /dev/null -H "Fastly-Debug:1"

https://developer.fastly.com/reference/http/http-headers/Fastly-Debug/

framework-awarenessのビルドだけ実行

npx firebase emulators:exec "exit 0"

next/imageが動かない

500エラーになる。以下のエラーが出ていた

Memory limit of 256M exceeded with 269M used. Consider increasing the memory limit, see https://cloud.google.com/run/docs/configuring/memory-limits

Google Cloudのコンソールから512Mに増やしてデプロイし直したら動作した。ビルドプロセスでこの部分を変更する方法はまだ用意されてなさそう。

料金が安くなる?

「Firebase Hostingにデプロイした方がVercelより安い」という話はまだみんな詳細確認してないと思う。

Firebaseコンソールからは見えづらいけど起っていることから考えると、Cloud Run使った時と同じ料金が発生するのだと思う。

「Next.jsのホスティング先としてFirebaseは『かなりアリ』な選択肢になっている」に書かれているとうり無料枠と上限と課金モデルがことなるので、比較してみるのは面白そうだ。

脚注
  1. https://zenn.dev/masakasuno1/articles/0988d547ab1de8 ↩︎

  2. https://zenn.dev/laiso/articles/8c619c38bd7b7b ↩︎

  3. https://zenn.dev/laiso/articles/438a9d1177ad52 ↩︎

  4. https://caniuse.com/?search=stale-while-revalidate ↩︎

Discussion