Firebase CLIのNext.jsデプロイ対応について調べる
Firebase HostingがNext.jsのデプロイに対応した[1] と聞きつけ、Next.jsビルドツール好き[2] [3] なので様子を見てきました。
のリポジトリを中心に調べてみます。
Firebase CLI framework-awareness とは
フレームワークサポートを付与するためのFirebase CLI のアドオン。
Firebaseプロジェクトの構成に応じて、Google Cloudのリソースを構築する。
現在Next, Nuxt2/3, AngularをサポートしていてCloud Functionsにこれからのフレームワーク機能をサポートするエンドポイントを自動でデプロイしてくれる。
内部アーキテクチャ
-
next export
で .next/ ディレクトリができる -
firebase-frameworks.build()
がプロジェクト構造を解析してフレームワークを検知 -
firebase-frameworks/frameworks/next.js/build()
でCloud Functionsへデプロイするアーティファクトに固める -
.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が存在する必要がある(後述)。
テストコードにあった以下をコピーした
/** @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が動いてる。
$ gcloud run services list
SERVICE REGION
ssrnextfirebasehostingswr us-central1
$ gcloud functions list
Listed 0 items.
# Runじゃん
その他のCloud Run設定
- regionはHosting経由で呼ぶのでus-central1になる
- 実行環境はCloud Run第1世代
stale-while-revalidateの挙動
READMEにSWR/E: ❌ と記載されているが、たぶんNext.js機能とのインテグレーションのことで、SWR自体は前から使えていた。
今回デプロイしたサイトではサンプルどうりにcache-controlヘッダを
cache-control: public, s-maxage=10, stale-while-revalidate=59
とした。
ブラウザから連続してアクセスして x-cache: HIT
かつ コンテンツが更新されたので意図どうりに動作していると判断した。
応答速度
Cloud Loggingから各リクエスト結果を見てみる。
- コールドスタートで 1-2s
- ウォーム時は 10-20ms
- CACHE HIT時はログが残らない
- Firebase Hosting(Fastly)のレイヤーまでしか到達しないので
となっていた。
コールドスタートは最小インスタンス数を1以上にするとか改善方法がありそう。
APPENDIX
FirebaseAwareServer
FirebaseAwareServerという便利機能がfirebase-frameworksの中にあって、プロジェクト内で @firebase/app
をimportしているとサーバーミドルウェアのレイヤーで組込まれる。
これが有効になるとSSRのrequestオブジェクトに
- req.firebaseApp
- req.currentUser
を注入してくれて、firebase.jsの初期化(initializeApp)やFirebase Authの認証処理(session)を自動で行ってくれる。
フレームワークのタイプの検知
dynamicImport()
内でプロジェクト内にあるファイルによって検知される。なのでnext.config.jsなしのプロジェクトで試したら検知してくれなかった。
nuxt3はソースコードはあるがたぶんまだ検知されない(2になる)。
ISRするには
Firebase Hostingはstale-while-revalidateに対応しているので非同期なキャッシュコントロールという意味では一部実現できています
ただstale-while-revalidateはブラウザが持つ機能なので、Safariが未対応だったりするので [4] ISRのようなサーバー側のシステムが作れると良さそうです。
Serverless Next.js Component のISR実装を読み解くと同じ方式にすることを考えると、以下のようなパイプラインを構築したいです。
- リクエストをトリガーにしてCloud RunからCloud Tasksの非同期処理をenqueueする
- Task内で該当のURLのキャッシュをパージする
- CDNのキャッシュを更新する
- 次のリクエストでレスポンスがキャッシュから返される
これはメルカリ Shopsで実現されているFastlyCache MSに近いと思います。
ただ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"
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は『かなりアリ』な選択肢になっている」に書かれているとうり無料枠と上限と課金モデルがことなるので、比較してみるのは面白そうだ。
Discussion