Firebase Hostingはデプロイ時に全ての静的ファイルをCDNキャッシュからパージする
この振る舞いを知らなかったせいで長年デプロイ時のバグに苦しんだのでメモ。
この記事を書いた3日後に、Cloud FunctionsでConcurrencyを設定できるCloud Function Gen2が発表されました。
以下は古いCloud Functionsについて記述しておりますので、ご了承ください。
デプロイするたびに数分間だけ画面が真っ白になる
HTMLは降ってくるのにJSやCSSなどのファイルだけが降ってこないために画面が真っ白になるという現象がデプロイする度に発生していた。
あなたが同じ現象に苦しんでいる場合、次の要素を全て満たすならば、このバグを踏んでいる可能性が高い。
- SSR(Server Side Rendering)している
- SSRのレスポンスをCDNにキャッシュしている
- JSのURLがデプロイの度に変わる(ハッシュを含むなど)
FirebaseでSSRしているなら上記の条件を全て満たす構成にすることは多いと思う(後述)。
しかしFirebaseでこれをやると、HTMLとJSのCDNキャッシュ存続期間にズレが生じてしまい、古いHTMLを見ているユーザーがJSを取得できずに画面が真っ白になる現象が発生する。
一般的に、JSのキャッシュ時間がHTMLのキャッシュ時間よりも長ければ、そんなことは起きない。
なぜそんな事が起きるのか。その理由がタイトルにあるFirebase Hostingの振る舞いである。
Firebase Hostingはデプロイ時に全ての静的ファイルをCDNキャッシュからパージする。
これはfirebase.json
のheaders
にどのようなCache-Control
ヘッダを付与していても関係ない。本来のキャッシュ存続期間が何時間残っていたとしても、デプロイされた瞬間にFirebaseのCDNからパージされる。
「静的ファイルを」というのが重要で、動的なHTMLはパージされない(というより、全ての動的なレスポンスをCDNからパージするのは不可能であろう)。
あなたはOGPを付与するためにindex.html
をCloud Functionsから配信しているだろうが、そこに書かれた<script src="...">
のURLは古いままだ。CDNに残っているからである。
以下は解決策を現実的な順にいくつか紹介する。
Cloud Runを使う
ある程度お金と手間はかかるが、無理にCloud Functionsは使わずCloud Runを使うべきだと思う。
いつの間にかFirebase HostingとCloud Runを接続することも可能になっていた。
Cloud FunctionsはConcurrencyが1なので、そもそもWebサービスのHTTPリクエストを捌くのには適していない(と中の人も言っていた…)。
SSRにはCloud Runを使おう。
CDNキャッシュを止める
SSRのレスポンスヘッダにCache-Control: no-store
を付ければCDNにキャッシュされなくなる。
HTMLは常に最新のものが降ってくるので、JSが404になることはない。
これはFirebaseを使っていなければごく当たり前な構成である。ほとんどのWebサービスはindex.html
をキャッシュしたりしない。
なぜFireabseではこれが難しいのかと言うと、Cloud Functionsはconcurrencyが1だからである。
インスタンス数の下限を指定することもできるが、コスパが悪いのであまりやらないと思う。
下限を指定していなければインスタンス数は0になる。当然コールドスタートになり、遅い。これではスケールしない。
こうなったらCDNにキャッシュさせて、リクエストを減らすしかない。
そういうわけで、CDNキャッシュを使うのがFirebaseでSSRする場合の処世術になっていたと思う。
どうしてこんなことになってしまうのかずっと理解できなかったが、そもそもCloud Functionsはこういう用途に向いていないのだ。Cloud Runの存在を知り、ようやくそれが理解できた。
Cloud Runを使おう。
JSファイルのハッシュを無くす
ファイル名が同じであれば、キャッシュがパージされても404を返すことはない。
つまりHTMLが古かろうとJSは最新のものが降ってくるというキャッシュ戦略である。
やったことはないので、上手くいくかどうかは分からない。
この方法の難点は、JSファイルをブラウザ側でキャッシュできないことだ。
JSファイルのURLが同じであれば、ブラウザはキャッシュ存続期間が続いている限り古いJSを使い続けてしまう。
例えば、fireabse.jsonのheadersはこういう指定になるだろう。
{
"source": "/static/**/*.js",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=0, s-maxage=86400" }
}
↑実際にはrevalidateさえすれば毎回DLする必要はないので、もう少しいい書き方があると思う。
JSも動的に生成する
JSを動的に生成し(た振りをして、)無理やり長いCache-Control
を付けて配信する方法もあるといえばある。HTMLが古かろうと、JSはもっと古いものもCDNに残っているから、404にならないという富豪的なキャッシュ戦略である。
この方法には大きな穴があるような気がして一番下に書いたのだが、何が穴なのかは実のところよく分からない。なんとなくFirebase HostingのCDNに頼り切りになるのは避けたいなと思っている。
Discussion