Статичні файли

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-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:

http
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.

Згідно з RFC 9110, If-None-Match має пріоритет над If-Modified-Since. Для файлів, які вже є в кеші в пам’яті, умовна перевірка виконується без жодного звернення до диска.

Запити Range (206)

Відповіді зі статичними файлами оголошують Accept-Ranges: bytes, а запити GET з однодіапазонним заголовком Range отримують лише запитані байти:

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: 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:

bash
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 — зчитуються та стискаються заново на кожен запит від клієнта, що приймає стиснення. Для дуже великих файлів поставте перед OxPHP CDN, щоб кешувати їх на периферії. Інший варіант — організувати ресурси так, щоб файли, які запитують найчастіше, не перевищували 1 MiB.

Повертається 304, хоча ви очікуєте 200

Відповідь 304 означає, що клієнт уже має актуальну версію. Це коректна поведінка. Якщо під час розробки потрібно примусово отримати свіжу відповідь, задайте STATIC_MAX_AGE=off, щоб не надсилати заголовки ETag і Last-Modified.

Приклад для Docker

compose.yaml
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 — потрібні сторінки він прочитає сам.

prompt
Налаштуй кешування статичних файлів OxPHP для мого конвеєра ресурсів. Прочитай сторінку `features/static-files` через MCP-сервер документації OxPHP. Перевір, чи збирання в цьому репозиторії генерує імена файлів із хешем, а потім дай мені значення `STATIC_MAX_AGE` і `STATIC_REVALIDATE` для продакшну та для розробки й поясни, що кожне з них змінює для браузера, який повертається на сайт.

Див. також