Flutter WebアプリをFirebase Hostingで配信する際の、最適なキャッシュ制御(Cache-Control)に関するドキュメントです。
※Flutter 3.22以降前提
Flutter Webにおけるキャッシュ制御には、主に以下の3つのアプローチが考えられます。
エントリポイントとなるファイル(index.html, *.json, flutter_service_worker.js, flutter_bootstrap.js など)のみに no-cache を付与し、サーバーに必ず変更有無(Etag)を問い合わせさせます。
- メリット: Flutter標準のビルドシステムとService Workerの機能に完全に乗っかっているため、設定がシンプルで保守コストが極めて低いです。
- デメリット: Service Workerの「バックグラウンドで新しいバージョンをダウンロードし、次回の起動時に適用する」という仕様により、強制アップデート画面を出している場合でも「1回目に古い画面が一瞬表示され、リロードして初めて新しい画面になる」というラグが発生することがあります。
ビルド終了後にカスタムスクリプトを走らせ、main.dart.js 等を main.dart.[ハッシュ].js にリネームし、index.html のパスも動的に書き換えます。実体ファイルには Cache-Control: max-age=31536000, immutable を指定します。
- メリット: キャッシュ不整合が物理的に発生しなくなり、1発目で必ず最新のアプリが立ち上がります。
- デメリット: ビルド手順が複雑化(パッチスクリプトのメンテが必要)し、Flutterのコア仕様(ブートストラップ等の構造)が変わった際にスクリプトが壊れて直す手間が発生します。
index.html などをビルド後に自動修正し、<script src="main.dart.js?v=1.0.1"> のように ?v= を付与します。
- メリット: 手法Bよりもカスタムスクリプトが簡単です。
- デメリット: プロキシサーバーや一部のブラウザ設定によってクエリアギュメントが無視され、強固なキャッシュが残存する(手法BやAほどの確実性はない)リスクがあります。
機能の保守性とFlutterフレームワークの進化への追従性を考慮すれば、「手法A」がベストの落とし所です。
下手にビルドスクリプトを複雑にして手法Bを取り入れると、今後のFlutterのアップデートでビルドフローが壊れるリスクの方が大きいため、本プロジェクトではFlutterチームが想定している標準的な運用(手法A)を採用しています。
さらに、最新のFlutter仕様に合わせ、flutter_bootstrap.js もno-cacheの対象に含めることで、ラグや不具合を最小化する微調整を行っています。
上記の手法Aの考え方に基づき、firebase.json にて、対象のFirebase Hostingサイトに対して以下のヘッダー設定を行なっています。
※ モダンなFlutter Web (v3.22以降) で使われる flutter_bootstrap.js も検証対象に含めています。
"headers": [
{
"source": "**/*.@(html|json|wasm|mjs)",
"headers": [
{
"key": "Cache-Control",
"value": "no-cache"
}
]
},
{
"source": "@(flutter_service_worker.js|flutter_bootstrap.js)",
"headers": [
{
"key": "Cache-Control",
"value": "no-cache"
}
]
}
]挙動:
no-cache は「キャッシュを保存しない」という意味ではなく、「キャッシュを使う前に、必ずサーバー(Firebase Hosting)へ変更がないか問い合わせる(Etag検証)」 という意味です。
理由:
- アプリへのアクセス時、ブラウザはまず
index.htmlやブートストラップのスクリプト群を読み込みます。 - サーバー側でファイルに変更がなければ、サーバーからは
304 Not Modified(データ本体なし)が即座に返されます。この通信は軽量で一瞬で終わります。 - 変更があった場合のみ、新しいファイルがダウンロードされます。
- これにより、「最新版があるかどうかのチェック」を確実かつ高速に行うことができます。
挙動:
上記以外の *.js などの実体ファイルには Cache-Control ヘッダーを上書き指定せず、デフォルトのキャッシュ挙動を許容します。ブラウザは一度ダウンロードしたファイルを強力にキャッシュして使い回します。
理由:
- エントリポイントの検証で「更新あり」と判定され、新しい
flutter_service_worker.jsが読み込まれると、Service Workerは内部に持っている新しいファイルのハッシュ値(例:main.dart.jsではなく一意のハッシュキー)に基づいて新しい実体ファイルだけをダウンロードします。 - 更新がない通常アクセスの場合は、各ブラウザがローカルにキャッシュしている数MBのJS/WASMファイルをそのまま使うため、無駄な通信が発生せず爆速でアプリが起動します。
// 【非推奨】絶対にやってはいけない設定
"source": "**/*.@(html|js)",
"headers": [
{
"key": "Cache-Control",
"value": "no-cache, no-store, must-revalidate"
}
]なぜダメなのか:
no-store を付与すると、ブラウザはキャッシュを一切保存しなくなります。
その結果、ユーザーが複数回アクセスするたびに、毎回数MBの main.dart.js 等の実体データをゼロからダウンロードすることになり、通信量の増大や、起動が毎回遅くなるという致命的なUX悪化を招きます。
必ず上述のような「役割を分けた」キャッシュ戦略を採用してください。