Статические файлы
OxPHP отдаёт статические файлы напрямую из корневого каталога документов, не запуская PHP. Каждый файл отдаётся с автоматическим определением MIME-типа, кэшированием в памяти для быстрого повторного доступа и полноценным HTTP-кэшированием: ETag, условные запросы и запросы Range для частичной загрузки.
Как это работает
Когда запрос соответствует статическому файлу:
- Файл найден — слой маршрутизации сопоставляет путь URL с файлом на диске
- Определение MIME — тип содержимого определяется по расширению файла
- Проверка кэша — кэш файлов проверяется перед обращением к файловой системе
- Условная проверка — если запрос содержит
If-None-MatchилиIf-Modified-Since, OxPHP вычисляет условие и может вернуть304 Not Modified, не отправляя тело ответа - Проверка Range — если запрос GET или HEAD содержит заголовок
Range, OxPHP отвечает206 Partial Content: GET получает только запрошенный диапазон байтов, HEAD — те же заголовки диапазона без тела - Ответ — файлы размером до 1 MiB отдаются из кэша в памяти; файлы большего размера передаются потоком напрямую с диска
Конфигурация
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
STATIC_MAX_AGE |
30d |
Cache-Control: max-age для статических файлов. Принимает 30s, 5m, 2h, 30d, 1w, 1y, просто число секунд (например, 3600) или off, чтобы полностью отключить заголовки кэширования. Заменяет устаревшую STATIC_CACHE_TTL. |
STATIC_REVALIDATE |
off |
Установите on, чтобы включить ревалидацию по mtime для кэша содержимого в памяти (перепроверяет каждый файл не чаще одного раза в 3 секунды; изменения становятся видимыми в пределах этого окна). Заменяет устаревшую STATIC_CACHE (где off имело обратное значение). |
Определение 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 использует кэш в памяти, чтобы снизить количество обращений к диску для часто запрашиваемых файлов:
- Файлы размером до 1 MiB (1 048 576 байт) считываются в память и кэшируются. Общий бюджет кэша составляет 64 MiB (67 108 864 байта). При превышении бюджета вытесняются записи, к которым дольше всего не обращались, чтобы освободить место.
- Файлы размером больше 1 MiB всегда передаются потоком напрямую с диска. Заголовок
Content-Lengthберётся из метаданных файла, поэтому клиент заранее знает полный размер.
Кэш файлов заполняется при первом запросе к каждому файлу и сохраняется между последующими запросами. По умолчанию записи кэша хранятся до тех пор, пока не будут вытеснены политикой LRU.
Ревалидация содержимого
Установите STATIC_REVALIDATE=on, чтобы включить ревалидацию по mtime. В этом режиме сервер перепроверяет время изменения кэшированного файла системным вызовом stat() не чаще одного раза в 3 секунды на файл, а не при каждом запросе. Если файл на диске изменился, устаревшая запись вытесняется, и файл автоматически считывается заново. В пределах 3-секундного окна кэшированная запись отдаётся прямо из памяти без системного вызова, поэтому затраты амортизируются, а не оплачиваются на каждом запросе. Изменения на диске становятся видимыми в течение 3 секунд.
Включите STATIC_REVALIDATE=on в процессе разработки, чтобы видеть изменения файлов без перезапуска сервера. Оставьте её невыставленной в продакшене (значение по умолчанию off) для максимальной пропускной способности с нулевыми накладными расходами на системные вызовы при каждом запросе.
HTTP-кэширование
Cache-Control
Когда задана STATIC_MAX_AGE (значение по умолчанию — 30d), каждый ответ со статическим файлом содержит заголовок Cache-Control:
Cache-Control: public, max-age=2592000Значение max-age — это TTL, переведённый в секунды. Установите STATIC_MAX_AGE=off, чтобы полностью убрать этот заголовок.
ETag и Last-Modified
Каждый ответ со статическим файлом содержит:
- ETag — сильный ETag в формате
"<size>-<mtime_hex>", вычисляемый из размера файла и времени последнего изменения. Сильный валидатор также удовлетворяетIf-Range, поэтому прерванные загрузки можно безопасно возобновлять. Когда ответ отдаётся сжатым brotli, тег ослабляется доW/"…"— сжатые байты представляют собой другое представление, и слабый тег по-прежнему позволяет выполнять ревалидацию (304), но предотвращает смешивание сжатых и несжатых фрагментов при возобновлении. - Last-Modified — HTTP-дата по RFC 7231 на основе времени изменения файла
Эти заголовки позволяют браузерам и CDN проверять кэшированные копии, не загружая файл повторно.
Условные запросы (304)
OxPHP анализирует заголовки условных запросов, чтобы не отправлять неизменившееся содержимое файла:
- If-None-Match — клиент отправляет закэшированный им ETag. Если он совпадает с текущим файлом, OxPHP возвращает
304 Not Modifiedбез тела. - If-Modified-Since — клиент отправляет метку времени. Если файл не изменялся с этого момента, OxPHP возвращает 304.
If-None-Match имеет приоритет над If-Modified-Since согласно RFC 7232. Для файлов, уже находящихся в кэше в памяти, условная проверка выполняется без каких-либо обращений к диску.
Запросы Range (206)
Ответы со статическими файлами объявляют Accept-Ranges: bytes, и запросы GET с заголовком Range, содержащим один диапазон, получают только запрошенные байты:
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: bytes=N-M, bytes=N- (от смещения до конца) и bytes=-N (последние N байт).
- Диапазон, который невозможно удовлетворить (начало за концом файла), возвращает
416 Range Not SatisfiableсContent-Range: bytes */<size>. - If-Range учитывается: когда клиент отправляет ETag (или дату
Last-Modified) своей частичной копии, а файл с тех пор изменился, OxPHP возвращает полный ответ200вместо несоответствующего фрагмента. Форма с датой принимается только после того, как секунда изменения файла полностью истекла — только что записанный файл может измениться снова в пределах той же секунды, не сдвинув дату, поэтому она пока не является сильным валидатором (RFC 9110). - Запросы с несколькими диапазонами (
bytes=0-1,4-5) получают файл целиком как200 OK— ответыmultipart/byterangesне генерируются. - Запросы HEAD с заголовком
Rangeполучают те же заголовки206/Content-Range, что и GET, но без тела — как в nginx и Apache. - Диапазоны и сжатие взаимоисключающи. Для клиентов, принимающих brotli, обработка диапазонов отключается для представлений, которые были бы отданы в сжатом виде, а сжатые ответы не объявляют
Accept-Ranges— иначе возобновлённая загрузка могла бы приклеить несжатые байты к сжатому префиксу. Сжимаются только файлы, отдаваемые из кэша в памяти (до 1 MiB), поэтому диапазоны всегда работают для содержимого, которому они действительно нужны: видео, архивов, изображений и любого файла, передаваемого потоком с диска. Ответы для файлов, подлежащих сжатию, всегда содержатVary: Accept-Encoding— даже когда отдаются без сжатия — чтобы разделяемые кэши хранили варианты раздельно. - Ответы
206никогда не сжимаются, а обработка диапазонов не применяется к ответам PHP — только к статическим файлам.
Пример: возобновление прерванной загрузки с помощью curl:
curl -C - -O https://example.com/dist/app-installer.dmgОтключение кэширования
Существуют два независимых слоя кэширования и по переменной для каждого:
| Переменная | Что контролирует | Эффект off |
|---|---|---|
STATIC_MAX_AGE=off |
Кэш браузера (HTTP-заголовки) | Заголовки Cache-Control, ETag и Last-Modified не отправляются |
STATIC_REVALIDATE=on |
Серверный кэш в памяти | Перепроверяет mtime файла не чаще одного раза в 3 секунды на файл; устаревшие записи вытесняются автоматически |
Для разработки установите 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. Добавьте файлу правильное расширение или убедитесь, что ваш фреймворк явно устанавливает заголовок Content-Type в ответах PHP.
Большие файлы работают медленно
Файлы размером больше 1 MiB передаются потоком с диска при каждом запросе и не кэшируются в памяти. Для очень больших файлов разместите CDN перед OxPHP, чтобы кэшировать их на границе сети. Как вариант, реорганизуйте свои ресурсы так, чтобы часто отдаваемые файлы оставались меньше 1 MiB.
Возвращаются ответы 304, когда вы ожидаете 200
304 означает, что у клиента уже есть текущая версия. Это корректное поведение. Если во время разработки нужно принудительно получить свежий ответ, установите STATIC_MAX_AGE=off, чтобы перестать отправлять заголовки ETag и Last-Modified.
Пример Docker
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). УстановитеSTATIC_MAX_AGE=1yдля максимального кэширования в браузере и CDN. - Устанавливайте
STATIC_REVALIDATE=onво время разработки, чтобы сервер обнаруживал изменения файлов автоматически. При желании также установитеSTATIC_MAX_AGE=off, чтобы обойти кэширование в браузере. - Размещайте CDN перед OxPHP для высоконагруженных сайтов. Заголовки
ETag,Last-ModifiedиCache-Controlработают со всеми крупными провайдерами CDN. - Доверьте хэширование ресурсов своему сборщику. Фреймворки вроде Vite и Laravel Mix генерируют имена файлов с хэшами автоматически, что делает длинные TTL кэша безопасными.
Смотрите также
- Сжатие — сжатие Brotli для сжимаемых ответов со статическими файлами
- Маршрутизация — как пути URL сопоставляются с файлами на диске
- Справочник по конфигурации — полный список переменных окружения