静的ファイル

OxPHP は PHP を呼び出すことなく、ドキュメントルートから静的ファイルを直接配信します。各ファイルは自動 MIME タイプ検出、繰り返しの高速アクセスのためのインメモリキャッシュ、そして完全な HTTP キャッシュ(ETag、条件付きリクエスト、部分ダウンロードのための Range リクエスト)とともに配信されます。

仕組み

リクエストが静的ファイルにマッチすると、次のように処理されます。

  1. ファイルのマッチ — ルーティング層が URL パスをディスク上のファイルに解決します
  2. MIME 検出 — ファイル拡張子からコンテンツタイプが判定されます
  3. キャッシュチェック — ファイルシステムに触れる前にファイルキャッシュが確認されます
  4. 条件付きチェック — リクエストが If-None-Match または If-Modified-Since を伴う場合、OxPHP はその条件を評価し、ボディを送信せずに 304 Not Modified を返す場合があります
  5. Range チェック — GET または HEAD リクエストが Range ヘッダーを伴う場合、OxPHP は 206 Partial Content で応答します。GET はリクエストされたバイト範囲のみを受け取り、HEAD は同じ範囲ヘッダーをボディなしで受け取ります
  6. レスポンス — 1 MiB 以下のファイルはインメモリキャッシュから配信され、それより大きいファイルはディスクから直接ストリーミングされます

設定

変数 デフォルト 説明
STATIC_MAX_AGE 30d 静的ファイルの Cache-Control: max-age30s5m2h30d1w1y、秒数を表す裸の数値(例: 3600)、またはキャッシュヘッダーを完全に無効化する off を受け付けます。非推奨の STATIC_CACHE_TTL を置き換えます。
STATIC_REVALIDATE off インメモリコンテンツキャッシュで mtime による再検証を有効化するには on に設定します(各ファイルを最大でも 3 秒に 1 回だけ再チェックし、変更はそのウィンドウ内で反映されます)。非推奨の STATIC_CACHEoff が逆の意味を持っていた)を置き換えます。

MIME 検出

MIME タイプはファイル拡張子から自動的に判定されます。タイプを判定できない場合、サーバーは application/octet-stream にフォールバックします。一般的なマッピングは次のとおりです。

拡張子 Content-Type
.html text/html
.css text/css
.js text/javascript
.json application/json
.png image/png
.svg image/svg+xml
.woff2 font/woff2

ファイルキャッシュ

OxPHP は、頻繁にリクエストされるファイルのディスク I/O を削減するためにインメモリキャッシュを使用します。

  • 1 MiB 以下(1,048,576 バイト)のファイルはメモリに読み込まれてキャッシュされます。キャッシュ予算の合計は 64 MiB(67,108,864 バイト)です。予算を超過すると、直近で最も使われていないエントリが退避されて空きが確保されます。
  • 1 MiB より大きいファイルは常にディスクから直接ストリーミングされます。Content-Length ヘッダーはファイルのメタデータから設定されるため、クライアントは全体のサイズを事前に把握できます。

ファイルキャッシュは各ファイルへの最初のリクエスト時に投入され、以降のリクエストにわたって保持されます。デフォルトでは、キャッシュエントリは LRU ポリシーによって退避されるまで保持されます。

コンテンツの再検証

STATIC_REVALIDATE=on を設定すると、mtime ベースの再検証が有効になります。このモードでは、サーバーはキャッシュされたファイルの更新時刻を stat() システムコールで再チェックしますが、これは毎リクエストではなくファイルごとに最大でも 3 秒に 1 回です。ファイルがディスク上で変更されていれば、古いエントリは退避され、ファイルは自動的に再読み込みされます。3 秒のウィンドウ内では、キャッシュされたエントリはシステムコールなしでメモリから直接配信されるため、コストはリクエストごとに支払われるのではなく分散されます。ディスク上の変更は 3 秒以内に反映されます。

開発

開発時には STATIC_REVALIDATE=on にすると、サーバーを再起動せずにファイルの変更が反映されます。本番環境では未設定のまま(デフォルトの off)にして、リクエストごとのシステムコールのオーバーヘッドをゼロにし、スループットを最大化してください。

HTTP キャッシュ

Cache-Control

STATIC_MAX_AGE が設定されている場合(デフォルトは 30d)、すべての静的ファイルレスポンスには Cache-Control ヘッダーが含まれます。

http
Cache-Control: public, max-age=2592000

max-age の値は TTL を秒に変換したものです。このヘッダーを完全に省略するには STATIC_MAX_AGE=off を設定します。

ETag と Last-Modified

すべての静的ファイルレスポンスには次のものが含まれます。

  • ETag"<size>-<mtime_hex>" の形式の強い ETag で、ファイルサイズと最終更新時刻から導出されます。強いバリデーターは If-Range も満たすため、中断されたダウンロードを安全に再開できます。レスポンスが brotli 圧縮で配信される場合、タグは W/"…" に弱められます。圧縮されたバイト列は異なる表現であり、弱いタグでも再検証(304)は行えますが、再開時に圧縮された断片と非圧縮の断片が混ざるのを防ぎます。
  • Last-Modified — ファイルの更新時刻に基づく RFC 7231 の HTTP 日付

これらのヘッダーにより、ブラウザや CDN はファイルを再ダウンロードすることなくキャッシュされたコピーを検証できます。

条件付きリクエスト(304)

OxPHP は、変更されていないファイルコンテンツの送信を回避するために条件付きリクエストヘッダーを評価します。

  • If-None-Match — クライアントはキャッシュしている ETag を送信します。それが現在のファイルと一致すれば、OxPHP はボディなしで 304 Not Modified を返します。
  • If-Modified-Since — クライアントはタイムスタンプを送信します。ファイルがその時刻以降に変更されていなければ、OxPHP は 304 を返します。

RFC 7232 に従い、If-None-MatchIf-Modified-Since より優先されます。すでにインメモリキャッシュにあるファイルの場合、条件付きチェックはディスク I/O なしで実行されます。

Range リクエスト(206)

静的ファイルレスポンスは Accept-Ranges: bytes を通知し、単一範囲の Range ヘッダーを伴う GET リクエストはリクエストされたバイトのみを受け取ります。

http
GET /videos/intro.mp4 HTTP/1.1 Range: bytes=1048576- HTTP/1.1 206 Partial Content Content-Range: bytes 1048576-52428799/52428800 Content-Length: 51380224

これにより、ブラウザでの <video>/<audio> のシーク、再開可能なダウンロード(wget -c、ダウンロードマネージャー)、そして PDF の部分読み込みが可能になります。RFC 9110 の 3 つの範囲形式すべてがサポートされています。bytes=N-Mbytes=N-(オフセットから末尾まで)、そして bytes=-N(末尾 N バイト)です。

  • 満たせない範囲(ファイルの末尾を超える開始位置)は、Content-Range: bytes */<size> を伴う 416 Range Not Satisfiable を返します。
  • If-Range が尊重されます。クライアントが自身の部分コピーの ETag(または Last-Modified 日付)を送信し、それ以降にファイルが変更されている場合、OxPHP は不一致の断片ではなく完全な 200 レスポンスを返します。日付形式は、ファイルの更新秒が完全に経過して初めて受け付けられます。書き込まれたばかりのファイルは、日付を動かさずに同じ秒内で再度変更される可能性があるため、まだ強いバリデーターではありません(RFC 9110)。
  • 複数の範囲bytes=0-1,4-5)を伴うリクエストは、200 OK としてファイル全体を受け取ります。multipart/byteranges レスポンスは生成されません。
  • Range ヘッダーを伴う HEAD リクエストは、GET と同じ 206/Content-Range ヘッダーをボディなしで受け取り、nginx や Apache と同じ挙動になります。
  • 範囲と圧縮は相互排他的です。 brotli を受け入れるクライアントに対しては、圧縮して配信される表現では範囲処理が無効化され、圧縮されたレスポンスは Accept-Ranges を通知しません。そうしないと、再開されたダウンロードが圧縮されたプレフィックスに非圧縮のバイトを継ぎ足してしまう可能性があるためです。圧縮されるのはインメモリキャッシュから配信されるファイル(1 MiB 以下)だけなので、範囲は実際にそれを必要とするコンテンツ、つまり動画、アーカイブ、画像、そしてディスクからストリーミングされるすべてのファイルに対して常に機能します。圧縮対象となりうるファイルのレスポンスは、非圧縮で配信される場合でも常に Vary: Accept-Encoding を伴うため、共有キャッシュはそれぞれのバリアントを区別して保持します。
  • 206 レスポンスは決して圧縮されず、範囲処理は PHP レスポンスには適用されません。静的ファイルにのみ適用されます。

例: curl で中断されたダウンロードを再開する:

bash
curl -C - -O https://example.com/dist/app-installer.dmg

キャッシュの無効化

独立した 2 つのキャッシュ層があり、それぞれに対応する変数があります。

変数 制御対象 off の効果
STATIC_MAX_AGE=off ブラウザキャッシュ(HTTP ヘッダー) Cache-ControlETagLast-Modified ヘッダーを送信しない
STATIC_REVALIDATE=on サーバーのインメモリキャッシュ ファイルの mtime をファイルごとに最大でも 3 秒に 1 回再チェックし、古いエントリを自動的に退避する

開発時には STATIC_REVALIDATE=on を設定して、サーバーが常に最新のコンテンツを配信するようにします。必要に応じて STATIC_MAX_AGE=off も設定して、ブラウザキャッシュを完全に防ぐこともできます。

トラブルシューティング

サーバーが古いファイルを配信し続ける

デフォルトでは、インメモリコンテンツキャッシュはファイルがディスク上で変更されたかどうかをチェックしません。開発中は STATIC_REVALIDATE=on を設定して mtime 再検証を有効にしてください。サーバーはファイルの変更を自動的に(3 秒以内に)検出します。

ブラウザが古いファイルを配信し続ける

サーバーは最新のコンテンツを返しているのにブラウザが依然として古いバージョンを表示する場合、原因はブラウザ自身のキャッシュです。STATIC_MAX_AGE=off を設定してキャッシュヘッダーの送信を停止するか、ブラウザのハードリロード(Shift+F5 または Cmd+Shift+R)を使用してください。

ファイルが `application/octet-stream` で配信される

OxPHP は MIME タイプの判定にファイル拡張子を使用します。拡張子が欠落しているか認識されない場合、application/octet-stream にフォールバックします。ファイルに正しい拡張子を追加するか、PHP レスポンスで Content-Type ヘッダーを明示的に設定するようフレームワークを構成してください。

大きなファイルが遅いように見える

1 MiB より大きいファイルはリクエストごとにディスクからストリーミングされ、メモリにはキャッシュされません。非常に大きなファイルの場合は、OxPHP の前段に CDN を配置してエッジでキャッシュしてください。あるいは、頻繁に配信されるファイルが 1 MiB 未満に収まるようにアセットを再構成してください。

200 を期待しているのに 304 レスポンスが返る

304 は、クライアントがすでに現在のバージョンを持っていることを意味します。これは正しい挙動です。開発中に強制的に最新のレスポンスを取得する必要がある場合は、STATIC_MAX_AGE=off を設定して ETagLast-Modified ヘッダーの送信を停止してください。

Docker の例

compose.yaml
services: app: image: ghcr.io/oxphp/oxphp:0.10.0 ports: - "8080:80" volumes: - ./src:/var/www/html environment: - DOCUMENT_ROOT=/var/www/html/public - ENTRY_FILE=index.php - STATIC_MAX_AGE=1y

ベストプラクティス

  • 本番環境ではキャッシュバスティング用のファイル名とともに長い TTL を使用してください(例: app.a1b2c3.js)。ブラウザと CDN のキャッシュを最大化するには STATIC_MAX_AGE=1y を設定します。
  • 開発中は STATIC_REVALIDATE=on を設定してください。これによりサーバーがファイルの変更を自動的に検出します。必要に応じて STATIC_MAX_AGE=off も設定して、ブラウザキャッシュをバイパスできます。
  • 高トラフィックのサイトでは、OxPHP の前段に CDN を配置してくださいETagLast-ModifiedCache-Control の各ヘッダーは、主要な CDN プロバイダーすべてで機能します。
  • アセットのハッシュ化はビルドツールに任せてください。 Vite や Laravel Mix のようなフレームワークはハッシュ化されたファイル名を自動的に生成するため、長いキャッシュ TTL を安全に使えます。

関連項目