Статические файлы
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берётся из метаданных файла, поэтому клиент заранее знает полный размер. - Единственное исключение из потоковой передачи — сжимаемый файл, который ещё укладывается в окно сжатия (3 MiB). Для клиентов, принимающих какое-либо кодирование содержимого, он считывается целиком, потому что сервер не может закодировать тело, которого у него нет полностью. Поскольку такой файл превышает лимит кэша, он сжимается заново при каждом таком запросе. Поставьте CDN перед ресурсами этого диапазона или держите их меньше 1 MiB — тогда работа выполняется один раз. Одновременно удерживается лишь ограниченное число таких файлов; запрос, пришедший сверх этого лимита, получает файл потоком и без кодирования, а не ждёт, пока освободится место.
Кэш файлов заполняется при первом запросе к каждому файлу и сохраняется между последующими запросами. По умолчанию записи кэша хранятся до тех пор, пока не будут вытеснены политикой 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, поэтому прерванные загрузки можно безопасно возобновлять. Когда ответ отдаётся сжатым, в любом из кодирований, тег ослабляется доW/"…": сжатые байты представляют собой другое представление, и слабый тег по-прежнему позволяет выполнять ревалидацию (304), но предотвращает смешивание сжатых и несжатых фрагментов при возобновлении. - Last-Modified — HTTP-дата по RFC 9110 на основе времени изменения файла
Эти заголовки позволяют браузерам и CDN проверять кэшированные копии, не загружая файл повторно.
Условные запросы (304)
OxPHP анализирует заголовки условных запросов, чтобы не отправлять неизменившееся содержимое файла:
- If-None-Match — клиент отправляет закэшированный им ETag. Если он совпадает с текущим файлом, OxPHP возвращает
304 Not Modifiedбез тела. - If-Modified-Since — клиент отправляет метку времени. Если файл не изменялся с этого момента, OxPHP возвращает 304.
If-None-Match имеет приоритет над If-Modified-Since согласно RFC 9110. Для файлов, уже находящихся в кэше в памяти, условная проверка выполняется без каких-либо обращений к диску.
Запросы 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. - Диапазоны и сжатие взаимоисключающи. Для клиентов, принимающих любое кодирование содержимого, обработка диапазонов отключается для представлений, которые были бы отданы в сжатом виде, а сжатые ответы не объявляют
Accept-Ranges, поскольку иначе возобновлённая загрузка могла бы приклеить несжатые байты к сжатому префиксу. Файлы больше окна сжатия (3 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 не кэшируются в памяти. Они передаются потоком с диска при каждом запросе либо, если они сжимаемые и меньше 3 MiB, считываются и сжимаются заново при каждом запросе от клиента, принимающего сжатие. Для очень больших файлов разместите CDN перед OxPHP, чтобы кэшировать их на границе сети. Как вариант, реорганизуйте свои ресурсы так, чтобы часто отдаваемые файлы оставались меньше 1 MiB.
Возвращаются ответы 304, когда вы ожидаете 200
304 означает, что у клиента уже есть текущая версия. Это корректное поведение. Если во время разработки нужно принудительно получить свежий ответ, установите STATIC_MAX_AGE=off, чтобы перестать отправлять заголовки ETag и Last-Modified.
Пример Docker
services:
app:
image: ghcr.io/oxphp/oxphp:0.13.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 кэша безопасными.
Промпты для вашего агента
Эти промпты рассчитаны на агента с подключённым MCP-сервером документации OxPHP — нужные страницы он прочитает сам.
Настрой кэширование статических файлов OxPHP под мою сборку ассетов. Прочитай страницу `features/static-files` через MCP-сервер документации OxPHP.
Проверь, выдаёт ли сборка в этом репозитории имена файлов с хешем, и дай значения `STATIC_MAX_AGE` и `STATIC_REVALIDATE` для продакшена и для разработки, объяснив, что каждое из них меняет для браузера, который пришёл повторно.Смотрите также
- Сжатие — как сжимаются сжимаемые статические файлы и как они кэшируются для каждого кодирования
- Маршрутизация — как пути URL сопоставляются с файлами на диске
- Справочник по конфигурации — полный список переменных окружения