Стиснення

OxPHP стискає HTTP-відповіді за допомогою Brotli, Zstandard або gzip — залежно від того, що приймає клієнт. Стиснення застосовується автоматично до фіксованого переліку типів вмісту (текст, JSON, XML, SVG, WebAssembly та кілька форматів шрифтів), тож обсяг переданих даних зменшується без жодних змін у коді вашого застосунку.

Як це працює

Заголовок Accept-Encoding клієнта зчитується один раз, ще до того, як запит потрапить у конвеєр маршрутизації, і зіставляється з кодуваннями, які пропонує сервер. Що відбувається далі, залежить від того, звідки беруться байти.

Кешований статичний файл, для якого вже є збережена стиснена копія, віддається безпосередньо з цієї копії. Під час запиту нічого не стискається, і жодна з наведених нижче перевірок не застосовується. Див. Кешовані статичні файли.

Будь-яка інша відповідь (вивід PHP, власна сторінка помилки, статичний файл, для якого ще немає збереженої копії) після формування проходить ті самі перевірки в такому порядку:

  1. Часткові відповіді пропускаються. Відповідь 206 ніколи не кодується, бо зсуви в її Content-Range описують незакодоване представлення.
  2. Перевірка типу вмісту. MIME-тип відповіді шукається в переліку типів, що стискаються.
  3. Перевірка на наявне кодування. Відповідь, яка вже має заголовок Content-Encoding, залишається без змін.
  4. Перевірка розміру. Стискаються лише тіла розміром від 256 байтів до 3 MiB (3 145 728 байтів) включно з обох боків. Якщо відповідь має заголовок Content-Length, використовується він; відповіді PHP зазвичай його не мають, тож для них вимірюється буферизоване тіло.
  5. Стиснення. Узгоджене кодування застосовується з налаштованим рівнем. Якщо стиснений результат не менший за оригінал, натомість надсилається нестиснена відповідь.
Note

Стиснення відбувається після виконання PHP, після віддачі статичних файлів і після підстановки власних сторінок помилок, тож власне тіло 404 понад 256 байтів стискається так само, як і будь-яка інша відповідь. Вбудовані тіла помилок (404 Not Found, 500 Internal Server Error) мають кілька десятків байтів і не дотягують до нижньої межі розміру. В усьому іншому код статусу ніколи не враховується.

Невеликі тіла стискаються безпосередньо; після певного порогу робота переходить у блокувальний потік, щоб не гальмувати асинхронне середовище виконання. Поріг становить 64 KiB і знижується до 4 KiB, щойно рівень стає достатньо високим, щоб крива витрат різко пішла вгору: понад якість 4 для Brotli, рівень 6 для gzip, рівень 9 для Zstandard. За стандартних значень Brotli вже перебуває в «крутій» половині, тож тіла Brotli понад 4 KiB стискаються поза асинхронним потоком.

flowchart TD
    AE["Accept-Encoding, read once per request"]
    W["Ranked by weight. A coding the server<br/>does not offer scores zero and drops out"]
    Q{"Is a stored copy<br/>being served?"}
    A["Tie broken br, then zstd, then gzip.<br/>Built once at br 11, zstd 19, gzip 9"]
    P["Tie broken zstd, then br, then gzip.<br/>Compressed now at the configured<br/>zstd 6, br 5, gzip 6"]
    H["Content-Encoding, recomputed Content-Length,<br/>Vary, weakened ETag, no Accept-Ranges"]
    AE --> W
    W --> Q
    Q -->|yes| A
    Q -->|no| P
    A --> H
    P --> H

Вибір кодування

Accept-Encoding ранжує кодування за вагою (RFC 9110 §12.5.3), і OxPHP читає його саме так:

  • Назви кодувань порівнюються без урахування регістру, а невідомі кодування ігноруються.
  • br;q=0 відмовляється від Brotli. Це не те саме, що просто не вказати кодування: * може підхопити пропущене кодування, але не те, від якого явно відмовилися.
  • * охоплює будь-яке кодування, не назване в заголовку явно, з указаною для нього вагою.
  • Вага, яку не вдається розібрати, залишає кодування з вагою за замовчуванням; некоректний параметр його не відкидає. Це важливо для q=0. Значення qvalue має щонайбільше три знаки після коми, тож q=0.000 відмовляється від кодування, тоді як q=0.0000 взагалі не є коректною вагою і читається як повне прийняття. Кодування, назване двічі, читається з першою вагою.
  • Серед кодувань, які клієнт приймає і які пропонує сервер, перемагає те, якому клієнт надав найвищу вагу. Кодування, вимкнене в налаштуваннях, отримує нульову оцінку ще до ранжування, тож клієнт, чиє улюблене кодування сервер не пропонує, переходить до наступного вибору, а не до нестисненої відповіді.
  • Заголовок, який нічого не приймає (порожній або *;q=0), дає незакодовану відповідь. OxPHP ніколи не відповідає 406 через кодування вмісту і ніколи не сприймає identity як токен.

Зазвичай ваги рівні, бо браузери надсилають усі підтримувані кодування без ваг. OxPHP розв’язує цю нічию залежно від того, що станеться зі стисненими байтами:

Відповідь Бажане кодування Чому
Віддається зі збереженої копії Brotli, потім Zstandard, потім gzip Байти стискаються один раз з максимальною якістю й далі віддаються з пам’яті, тож важить лише розмір, а на вершині свого діапазону Brotli дає найменший результат із трьох
Усе інше Zstandard, потім Brotli, потім gzip Байти стискаються, поки клієнт чекає, і потім відкидаються, тож ціна сплачується за кожен запит. На рівнях, які може собі дозволити запит, Zstandard за вимірюваннями відставав від Brotli за розміром лише на кілька відсотків, витрачаючи значно менше половини CPU

Перший рядок діє лише тоді, коли збережена копія вже існує. Звернення до її появи, а також кожен файл, завеликий для кешування, підпадають під другий рядок — тому статичний ресурс зазвичай спочатку віддається в Zstandard, а вже потім у Brotli.

Пріоритет лише розв’язує нічию. Клієнт, який надсилає br;q=0.2, gzip;q=0.9, отримує gzip на обох шляхах, і збережена копія, створена для нього, теж буде в gzip.

Клієнт, який не приймає жодного з трьох кодувань, отримує незакодовану відповідь. На практиці запасним варіантом є gzip: його приймає кожен HTTP-клієнт останніх двадцяти років, тоді як Zstandard потребує браузера, випущеного у 2024 році або пізніше.

Звичайний HTTP — це сайт на gzip

Браузери вмикають новіші кодування залежно від з’єднання, а не лише від власного віку: br і zstd оголошуються через HTTPS і не оголошуються через звичайний HTTP. Сайт, що працює без TLS, — це сайт на gzip, хоч би якими браузерами користувалися його відвідувачі.

Vary: Accept-Encoding

Оскільки результат залежить від заголовка запиту, кожна відповідь у межах вікна стиснення має Vary: Accept-Encoding — стиснена чи ні, статична чи динамічна. Представлення, яке сервер закодував би для якогось клієнта, залежить від цього заголовка, навіть якщо поточному клієнту віддаються незакодовані байти, а спільний кеш, якому сказали б інакше, передав би ці байти клієнту, що просив кодування.

За межами цього правила залишаються два випадки:

  • Динамічна відповідь, яку сервер ніколи не тримає повністю (усе, що передається потоком у міру того, як PHP його генерує), не має Vary: на цьому шляху стиснення неможливе за жодного заголовка, тож відповідь справді не змінюється. Статичний файл, достатньо великий для потокової передачі, до цієї групи не належить і має цей заголовок, бо той самий файл читається повністю й стискається для клієнта, який узгодив кодування.
  • Відповідь 304 не має ані Vary, ані Content-Encoding: вона оновлює представлення, яке клієнт уже має, а не описує нове.

Vary додається окремим рядком заголовка, а не об’єднується з тим, який застосунок уже надіслав, тож відповідь, що встановила Vary: Cookie, виходить із двома рядками Vary. Це допустимо, і OxPHP перед додаванням читає їх усі, щоб Accept-Encoding ніколи не дублювався, але проміжний вузол, який читає лише перший рядок, його пропустить.

Кешовані статичні файли

Статичний файл, що вміщується в кеш вмісту (1 MiB або менше) і придатний для стиснення (тип із переліку, щонайменше 256 байтів), стискається один раз, а не за кожного запиту.

  1. 1
    Перший запит: промах кешу. Файл читається з диска, зберігається і стискається для цього запиту так само, як і все інше.
  2. 2
    Наступний запит: запуск створення копії. Файл уже в кеші, і якщо для кодування, узгодженого цим запитом, ще немає збереженої копії, сервер доручає створити її у фоновому потоці. Запит, що запустив створення, обслуговується звичайним способом — стисненням під запит, — як і кожен запит, що надходить, поки копія створюється.
  3. 3
    Копія готова. Подальші запити, що узгоджують те саме кодування, отримують відповідь зі збереженої копії.

Лічильника звернень немає: тригером є перше влучання в кеш без збереженої копії для цього кодування, тож файл, який досі віддавався лише клієнтам gzip, отримує копію Brotli під час першого звернення клієнта з Brotli.

Збережені копії створюються з максимальним рівнем кожного кодування (Brotli 11, Zstandard 19, gzip 9) незалежно від налаштованих рівнів для запитів, які збережена копія ніколи не використовує. Окремого параметра для них немає: єдиний спосіб не допустити створення копії — повністю вимкнути кодування. Максимальна якість зазвичай дає на 8–12% менше байтів, ніж рівень Brotli для запитів.

Кожне кодування отримує власну збережену копію, що створюється на вимогу, тож файл, який віддається лише клієнтам gzip, ніколи не коштуватиме стиснення Brotli. Усі копії мають спільний валідатор кешованого файлу й відкидаються разом із кешованими байтами: коли запис витісняється, щоб не перевищити бюджет кешу в 64 MiB (у який зараховуються і збережені копії), або коли увімкнено STATIC_REVALIDATE і він помічає зміну файлу на диску. Цей параметр за замовчуванням вимкнений, тож у стандартному розгортанні збережена копія відкидається лише у двох випадках — через витіснення або перезапуск. Байти, що не стискаються, позначаються як такі й повторно не обробляються, а відповідь 304 не запускає створення копії й не віддає збережену копію.

Кодування змінюється один раз для кожного файлу

Оскільки два правила розв’язання нічиєї різні, клієнт, що приймає і Brotli, і Zstandard, зазвичай бачить, як кодування змінюється один раз для кожного файлу: перші звернення отримують Zstandard, стиснений спеціально для цього запиту, а звернення після появи збереженої копії — Brotli. Обидва варіанти є коректними представленнями того самого ресурсу й обидва мають Vary, тож нічого не ламається. Вони також мають спільний слабкий ETag, тож кеш, що зберігає варіант у Zstandard, успішно проходить повторну перевірку й зберігає його; зміна стосується нових клієнтів, а не тих, що вже зберегли копію.

Файли, завеликі для кешування

Між межею кешу та верхньою межею вікна стиснення (від 1 MiB до 3 MiB — діапазон, де живуть бандли фреймворків, source maps і модулі WebAssembly) файл читається повністю і стискається за кожного запиту, якщо клієнт узгодив кодування, а тип придатний для стиснення. Такий файл не кешується й ніколи не отримує збереженої копії, тож це найдорожче стиснення, яке сервер виконує на один запит.

Tip

Поставте CDN перед ресурсами розміром від 1 до 3 MiB або тримайте їх у межах ліміту кешу в 1 MiB — і робота виконуватиметься один раз.

Під час читання такий файл утримується в пам’яті, доки відповідь не буде записано, тож одночасно читається лише обмежена кількість таких файлів. Запит, що надходить понад цей ліміт, отримує файл потоком і без кодування, а не чекає на вільне місце. Тому за тривалого навантаження на великий ресурс одні клієнти отримують його стисненим, а інші — ні; обидві відповіді коректні для цієї URL-адреси, і Vary: Accept-Encoding є в них у будь-якому разі.

За межами цього випадку файл понад 1 MiB передається потоком із диска без змін: тип, що не стискається, клієнт, який не приймає жодного кодування, або будь-що понад 3 MiB.

Скільки коштує статичний файл залежно від розміру:

flowchart TD
    F["Static file"]
    C1{"256 B or larger?"}
    C2{"1 MiB or smaller?"}
    C3{"3 MiB or smaller?"}
    N1["Sent as-is, and cached.<br/>The framing would cost more than it saves"]
    N2["Cached, and a stored copy is built once<br/>in the background on the first cache hit"]
    N3["Read whole and compressed on every request.<br/>Too large to cache, so no copy is kept"]
    N4["Streamed from disk, never compressed.<br/>Past the compression window"]
    F --> C1
    C1 -->|no| N1
    C1 -->|yes| C2
    C2 -->|yes| N2
    C2 -->|no| C3
    C3 -->|yes| N3
    C3 -->|no| N4

Ця схема передбачає тип, придатний для стиснення, і клієнта, який узгодив кодування. Приберіть будь-яку з цих умов — і файл кешується до 1 MiB, понад цю межу передається потоком, діапазони підтримуються всюди, і ніде нічого не кодується.

Налаштування

Змінна За замовчуванням Опис
COMPRESSION_ENCODINGS br,zstd,gzip Які кодування пропонує сервер, через кому. Приймає br (або brotli), zstd і gzip без урахування регістру та з будь-якими пробілами навколо. Порядок, у якому їх записано, ігнорується; сервер обирає для кожної відповіді окремо (див. Вибір кодування). Щоб повністю вимкнути стиснення, задайте все значення як off (або none). off не є елементом списку, тож br,off відхиляється як помилка
COMPRESSION_BROTLI_LEVEL 5 Якість Brotli (від 0 до 11)
COMPRESSION_ZSTD_LEVEL 6 Рівень Zstandard (від 0 до 19)
COMPRESSION_GZIP_LEVEL 6 Рівень gzip (від 0 до 9)
COMPRESSION_LEVEL (не задано) Застаріла назва COMPRESSION_BROTLI_LEVEL. Якщо її задано, під час запуску записується попередження в журнал. Явно заданий COMPRESSION_BROTLI_LEVEL перекриває ненульове значення, але не COMPRESSION_LEVEL=0, яке вимикає все стиснення, хоч би що ще було задано

Кодування пропонується, якщо воно є в COMPRESSION_ENCODINGS і його рівень не дорівнює 0; порушення будь-якої з цих умов вимикає його. Вимкнене кодування зникає з усієї функції: воно ніколи не узгоджується, ніколи не стискається на шляху запиту й ніколи не створюється як збережена копія.

Значення перевіряються, а не виправляються

Невідома назва кодування, рівень поза допустимим діапазоном або рівень, що не є числом, перериває запуск із помилкою, яка називає змінну. Нічого не обрізається до меж і нічого не відкидається мовчки. Порожнє присвоєння (COMPRESSION_ENCODINGS= — саме таку форму дає ${VAR:-}, коли змінної на хості немає) вважається незаданим значенням і відновлює значення за замовчуванням. Воно не означає off.

Перехід з COMPRESSION_LEVEL

Попередні випуски мали одне кодування й один параметр: COMPRESSION_LEVEL — якість Brotli зі значенням 4 за замовчуванням, де 0 вимикало стиснення. Змінна досі зчитується з обома значеннями, тож наявні розгортання продовжують працювати, але записує в журнал попередження про застарілість.

  • COMPRESSION_LEVEL=N (ненульове) стає COMPRESSION_BROTLI_LEVEL=N.
  • COMPRESSION_LEVEL=0 стає COMPRESSION_ENCODINGS=off.

Якщо залишити старе значення, Zstandard і gzip також залишаться увімкненими зі стандартними рівнями, і зазвичай саме це вам і потрібно: клієнти, що не приймали Brotli, раніше отримували нестиснені відповіді, а тепер отримують gzip.

Чому саме такі значення за замовчуванням

  • Brotli 5, а не 4. Точка перелому якості Brotli (зміна хешера) лежить між ними. На рівні 4 він давав більше байтів, ніж gzip на власному стандартному рівні, для JSON понад 4 KB і для реальних мініфікованих ресурсів, та ще й витрачав на це більше CPU, тож жодних причин віддавати йому перевагу над gzip не залишалося. На рівні 5 він дає менший результат з-поміж двох на кожному виміряному тілі приблизно за подвійну ціну gzip у CPU.
  • Gzip 6. Власне значення за замовчуванням zlib, близьке до точки спадної віддачі. Рівень 9 коштує приблизно вдвічі більше заради одного-двох відсотків.
  • Zstandard 6, а не його власне значення за замовчуванням 3. На тілах понад кілька кілобайтів рівень 6 витрачає менше часу, ніж якість Brotli, з якою попередні випуски стискали все, і водночас дає менше байтів, тож порівняно з тим, що надсилали ті випуски, нічого не погіршується.

Рівні від 9 до 11 краще підходять для стиснення офлайн або під час збирання, ніж для роботи на кожен запит; кешовані статичні файли все одно їх використовують, бо ця ціна сплачується один раз.

Типи вмісту, що стискаються

Перелік закритий: правил за префіксом чи шаблоном немає, тож тип, якого в ньому немає, ніколи не стискається, навіть якщо стискався б добре. У межах переліку порівняння поблажливе. Параметри й пробіли навколо відкидаються, а сам тип порівнюється без урахування регістру, тож і text/html; charset=utf-8, і Application/JSON збігаються.

Текстові типи:

  • text/html
  • text/css
  • text/plain
  • text/xml
  • text/javascript

Типи application:

  • application/javascript
  • application/json
  • application/xml
  • application/xhtml+xml
  • application/rss+xml
  • application/atom+xml
  • application/manifest+json
  • application/ld+json
  • application/wasm

Інші типи:

  • image/svg+xml
  • font/ttf
  • font/otf
  • application/x-font-ttf
  • application/x-font-opentype
  • application/vnd.ms-fontobject

Що не стискається

Відповіді надсилаються без стиснення, якщо виконується будь-яка з таких умов:

  • Клієнт не приймає жодного з br, zstd і gzip у заголовку Accept-Encoding, приймає їх лише з нульовою вагою (br;q=0, zstd;q=0, gzip;q=0) або взагалі не надсилає такого заголовка
  • Кожне кодування, яке приймає клієнт, вимкнене через COMPRESSION_ENCODINGS або рівнем 0
  • Відповідь уже має заголовок Content-Encoding (наприклад, попередньо стиснений вміст, створений застосунком)
  • Тіло відповіді менше за 256 байтів або більше за 3 MiB
  • Відповідь має статус 206 Partial Content: зсуви діапазонів описують незакодоване представлення, тож часткове тіло ніколи не кодується
  • Типу вмісту немає в переліку вище. image/png, image/jpeg, font/woff2 і application/zip виключено, бо ці формати вже мають власне стиснення, а text/event-stream — бо потік подій має доходити до клієнта подія за подією. text/csv, application/pdf, image/x-icon і відповідь без Content-Type узагалі теж виключено, бо їх немає в переліку
  • Сервер не тримає тіло повністю: PHP-скрипти, що використовують oxphp_stream_flush(), Server-Sent Events і статичні файли, які читаються з диска в міру надсилання. OxPHP стискає відповідь цілком. Brotli, Zstandard і gzip уміють кодувати поступово, тож це обмеження сервера, а не кодувань
  • Відповідь надійшла від плагіна, який відповів ще до запуску конвеєра, — наприклад, 429 через обмеження частоти запитів, що повертається до етапу стиснення
  • Відповідь надійшла від внутрішнього слухача (INTERNAL_ADDR), який віддає /metrics і ендпоінти перевірки стану без кодування незалежно від того, що приймає збирач метрик

Заголовки відповіді

Відповідь, яку кодує сервер, змінюється так — незалежно від того, чи її стиснено для цього запиту, чи віддано зі збереженої копії:

Заголовок Значення
Content-Encoding br, zstd або gzip — те, що було узгоджено
Content-Length Оновлюється до розміру стисненого тіла
Vary Додається Accept-Encoding, тож HTTP-кеші зберігають окремі версії для кожного кодування
ETag Сильний тег послаблюється до W/"…". Закодовані байти є іншим представленням, і сильний тег, спільний із незакодованими байтами, дав би змогу клієнту продовжити закодоване завантаження незакодованими фрагментами
Accept-Ranges Видаляється. Зсуви в байтах не мають сенсу для закодованого тіла

Усі кодування одного статичного файлу мають цей спільний слабкий тег, обчислений із незакодованого розміру й часу модифікації. Варіанти розрізняє саме Vary; тег лише визначає свіжість, тож кеш, що повторно перевіряє один варіант, не може повідомити серверу, який саме з них він зберігає.

Діапазони та стиснення

Обробка діапазонів вимикається ще до формування відповіді для кожної відповіді, яка була б стиснена для цього клієнта. Статичний файл, придатний для стиснення, у межах вікна стиснення віддається повною відповіддю 200 клієнту, що надсилає і Range, і Accept-Encoding, а незадовольний діапазон від такого клієнта теж отримує цю 200, а не 416. Клієнти, що не приймають жодного кодування, зберігають підтримку діапазонів — саме там і живуть відновлювані завантаження.

Спостережуваність

Два лічильники на ендпоінті метрик показують, що насправді зробило стиснення:

  • oxphp_compressed_responses_total: відповіді, надіслані з кодуванням вмісту
  • oxphp_compression_bytes_saved_total: незакодований розмір мінус стиснений розмір, у сумі

Обидва враховують як віддачу зі збережених копій, так і стиснення під запит, тож співвідношення з oxphp_requests_total відповідає на питання «чи охоплює стиснення мій трафік» надійніше, ніж окремий curl -I.

Усунення несправностей

Відповіді не стискаються

Переконайтеся, що клієнт узагалі надсилає заголовок Accept-Encoding. Браузери це роблять, але деякі інструменти для тестування HTTP за замовчуванням його не надсилають, а запит без нього отримує незакодовану відповідь.

Нульова вага означає відмову від кодування: Accept-Encoding: br;q=0, zstd;q=0, gzip;q=0 вимикає стиснення для цього запиту так само, як і відсутність заголовка.

Перевірте за допомогою curl:

bash
curl -H "Accept-Encoding: br, zstd, gzip" -I http://localhost/

Шукайте Content-Encoding у заголовках відповіді. Якщо його немає, перевірте, що:

  1. COMPRESSION_ENCODINGS досі містить потрібне вам кодування, а його рівень не дорівнює 0
  2. Тіло відповіді має щонайменше 256 байтів і не більше за 3 MiB
  3. Content-Type відповіді є в переліку типів, що стискаються, наведеному вище. Порівняння ігнорує регістр і параметри
  4. Обробник не передає дані потоком. Скрипт, що викликає oxphp_stream_flush(), надсилає Server-Sent Events або якось інакше починає писати, ще не знаючи довжини, пропускається без змін — і в застосунку, що скидає буфер, це найпоширеніша причина
Різні клієнти отримують різні кодування

Саме це й має давати узгодження, і майже всі такі випадки зводяться до трьох. Браузери не оголошують br і zstd через звичайний HTTP: там вони надсилають Accept-Encoding: gzip, deflate і отримують gzip, хоч би якими новими вони були. Інструменти командного рядка часто взагалі не надсилають Accept-Encoding і отримують незакодовану відповідь. А через HTTPS браузер, достатньо новий, щоб надсилати zstd, отримує Zstandard для динамічної відповіді там, де старіший отримує Brotli. У жодному з цих випадків нічого не налаштовано неправильно, а стиснені відповіді мають Vary: Accept-Encoding, тож кеші тримають їх окремо.

Кодування того самого статичного файлу змінюється

Так і має бути: перші звернення до кешованого статичного файлу стискаються Zstandard спеціально для відповіді на цей запит, а щойно у фоні буде створено збережену копію в Brotli, подальші звернення отримують відповідь з неї. Див. Кешовані статичні файли.

Запит діапазону повертає весь файл

Так і має бути для файлу, придатного для стиснення, у межах вікна стиснення, коли клієнт також надсилає Accept-Encoding: представлення, яке йому віддається, закодоване, і зсуви в байтах незакодованого файлу його не описують. Той самий запит без Accept-Encoding отримує свою 206. Див. Заголовки відповіді.

Стиснення збільшує відповіді

Тіло, що не стискається (blob у base64, набір випадкових ідентифікаторів, уже стиснене зображення, загорнуте як image/svg+xml), після будь-якого з трьох кодувань може вийти не меншим, ніж було. OxPHP порівнює обидва варіанти й надсилає оригінал щоразу, коли стиснення не допомогло, тож налаштовувати тут нічого не треба. Це не може бути проблемою малих відповідей: 256 байтів — нижня межа, нижче якої стиснення взагалі не намагаються застосувати.

Пропускна здатність знизилася після ввімкнення стиснення

Стиснення може як підвищити, так і знизити пропускну здатність. У який бік — залежить від того, чого бракувало серверу до його ввімкнення, і той самий параметр на тому самому сервері може змінити пропускну здатність у кілька разів у будь-який бік.

Для перевантаженого каналу це найбільший можливий виграш: текстова відповідь, що зменшилася у вісім разів, доставляється у вісім разів швидше, а CPU, який на неї витрачено, однаково простоював в очікуванні мережі.

Для вузького асинхронного середовища виконання це витрата. Тіло, менше за поріг передачі, стискається в асинхронному робочому потоці, а середовище виконання за замовчуванням займає половину CPU, доступних процесу, тож ця робота забирає ресурси в пулу, що обслуговує всі відкриті з’єднання, тоді як решта машини може мати вільні потужності. Заповнити його нескладно: два потоки середовища виконання, що стискають тіла JSON по 1,5 KB, насичуються однією лише цією роботою. Симптом, за яким це можна впізнати: пропускна здатність падає, а машина й пул робочих процесів PHP стають менш завантаженими, бо закінчився ресурс не в них.

Що перевірити: чи не вужче середовище виконання за машину. TOKIO_WORKERS за замовчуванням дорівнює половині доступних ядер, і його збільшення дає цій роботі змогу використовувати більшу частину машини. Наскільки більшу — залежить від машини й від того, що ще на ній працює, тож вимірюйте, а не припускайте, і водночас вимірюйте пул робочих процесів: його потоки відокремлені від потоків середовища виконання, тож у розгортанні, яке вже впирається в CPU через PHP, ширше середовище виконання забирає цикли в застосунку, а не знаходить нові.

Передача малих тіл у блокувальний пул цього не виправляє. У середовищі виконання з двома потоками тіло JSON на 1,5 KB, стиснене безпосередньо, займає близько 7 мкс потоку середовища виконання; передане в блокувальний пул і забране назад, воно займає 13–19 мкс, бо надсилання роботи, паркування й пробудження самі є роботою середовища виконання. Пропускна здатність такого середовища падає до третини чи половини, а очікування всього іншого в ньому зростає, а не зменшується. Пороги розміру встановлено з урахуванням цієї ціни.

Високе навантаження на CPU через стиснення

Ціна стиснення різко зростає на вершині діапазону кожного кодування (Brotli від 8 до 11, gzip 8 і 9, Zstandard від 12 до 19), а стандартні рівні для запитів лежать значно нижче. Якщо ви спостерігаєте високе споживання CPU через стиснення:

Виправлення: знизьте COMPRESSION_ZSTD_LEVEL до 3 і COMPRESSION_GZIP_LEVEL до 4. Brotli — найдорожче з трьох кодувань, але зниження COMPRESSION_BROTLI_LEVEL до 4 — не спосіб заощадити цей CPU: на рівні 4 він дає більше байтів, ніж gzip на власному стандартному рівні, і все одно коштує дорожче за gzip. Натомість приберіть br з COMPRESSION_ENCODINGS. Клієнти, достатньо сучасні, щоб надсилати zstd, перейдуть на Zstandard, а не на gzip, тож для них ця зміна нічого не коштує; на gzip перейдуть лише клієнти, що приймають Brotli без Zstandard (Safari, браузери, старші за 2024 рік, більшість проміжних вузлів), а на реальних тілах він більший на 2–6% приблизно за половину CPU.

Кешовані статичні файли до цієї ціни не належать: їхні стиснені копії створюються у фоні один раз, хоч би які кодування залишилися. А от файли від 1 MiB до 3 MiB — належать. Вони стискаються за кожного запиту й не можуть бути кешовані; див. Файли, завеликі для кешування.

Попередньо стиснені ресурси стискаються повторно

OxPHP не шукає файлів .br, .zst чи .gz поруч з оригіналом: на запит /app.js ніколи не віддається /app.js.br, а прямий запит стисненого файлу віддає його як непрозоре завантаження, яке браузери не декодуватимуть. Стисненню статичних ресурсів під час збирання немає куди під’єднатися; цю роль виконують власні збережені копії сервера — з тією самою максимальною якістю й без етапу збирання.

Пропуск повторного стиснення стосується відповідей, які вже мають заголовок Content-Encoding, а на практиці це означає PHP-скрипт, який сам закодував своє тіло й сам установив цей заголовок.

Приклад для 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 - COMPRESSION_ENCODINGS=br,zstd,gzip - COMPRESSION_BROTLI_LEVEL=5 - COMPRESSION_ZSTD_LEVEL=6 - COMPRESSION_GZIP_LEVEL=6

Промпти для вашого агента

Ці промпти розраховані на агента з підключеним MCP-сервером документації OxPHP — потрібні сторінки він прочитає сам.

prompt
З’ясуй, що саме OxPHP стискатиме в моєму застосунку, і налаштуй кодування. Прочитай сторінку `features/compression` через MCP-сервер документації OxPHP. Пройдися типами вмісту й розмірами відповідей, які повертає цей репозиторій, скажи, які з них виходять за межі переліку типів, що стискаються, або вікна від 256 B до 3 MiB, познач статичні ресурси розміром від 1 MiB до 3 MiB, які повторно стискаються за кожного запиту, і порекомендуй `COMPRESSION_ENCODINGS` та рівні для кожного кодування відповідно до структури трафіку, який він обслуговує. Якщо конфігурація досі задає застарілий `COMPRESSION_LEVEL`, перенеси його.

Див. також