PHP лишається мовою «кожен запит з нуля»: інтерпретатор читає файл, проганяє його через лексер, парсер і компілятор, отримує масив опкодів і лише тоді починає їх виконувати. Для фреймворку це сотні файлів на кожен запит — і вся ця компіляція викидається одразу після відповіді. Zend OPcache (у ядрі з PHP 5.5) прибирає саме цей крок: скомпільовані опкоди складаються у спільну памʼять (shared memory), доступну всім воркерам PHP-FPM того ж майстра, і наступний запит бере готове. Важливо правильно назвати межу: OPcache кешує опкоди, а не результат — код усе одно виконується щоразу, запити до бази робляться щоразу, шаблони рендеряться щоразу. Саме тому це «безкоштовні» десятки відсотків, а не чарівна кнопка.
Другий, менш очевидний виграш — файлові перевірки. За замовчуванням opcache.validate_timestamps=1 і opcache.revalidate_freq=2: раз на дві секунди OPcache робить stat() на кожен включений файл, щоб помітити зміни. На локальній машині це необхідно, у продакшені — чистий податок на syscalls, тим більший, чим повільніша файлова система (класично боляче на NFS і на монтованих томах у Docker). У продакшені ставлять opcache.validate_timestamps=0, і разом із цим приймають зобовʼязання: код у скомпільованому вигляді відтепер змінює тільки перезапуск. Деплой має закінчуватись graceful reload PHP-FPM (SIGUSR2) або підняттям нового контейнера; opcache_reset() теж працює, але скидає кеш різко для всіх воркерів одночасно, даючи сплеск CPU на перекомпіляції. Пропущений reload — це та сама класична історія «задеплоїли, а на сайті старий код»; жодних помилок у логах при цьому не буде.
Preload (opcache.preload, PHP 7.4+) іде на крок далі. Указаний файл виконується один раз під час старту майстер-процесу — до появи будь-якого запиту, від користувача opcache.preload_user, якщо PHP стартує від root. Усе, що цей файл завантажив через require або opcache_compile_file(), лишається в памʼяті звʼязаним: ієрархія класів побудована, інтерфейси й трейти підключені, тож при запиті такі класи доступні без автозавантажувача взагалі. Обмеження варто озвучувати самому, не чекаючи уточнення: класи, чиї батьки або інтерфейси не резолвляться на момент preload, тихо пропускаються (Can't preload unlinked class); preload-скрипт не повинен робити нічого, що залежить від конкретного запиту; на Windows механізм не підтримується; і головне — оновити preload-код можна лише рестартом, opcache_reset() його не чіпає. Через це preload у dev-оточенні — вірний спосіб згаяти день на «мої правки не застосовуються». Реалістична вигода — одиниці відсотків на середньому фреймворку; Symfony генерує preload.php із prod-контейнера з коробки, Laravel — ні.
JIT (PHP 8.0+) — теж частина OPcache, і в цьому вже половина відповіді. Він працює не з вихідним кодом, а з опкодами: гарячі ділянки транслюються в машинний код у буфер opcache.jit_buffer_size. Найчастіша плутанина на співбесіді — opcache.jit=tracing виглядає як «увімкнено», але за замовчуванням opcache.jit_buffer_size=0, і JIT просто вимкнений; вмикає його саме буфер. Чому веб від нього майже нічого не отримує: у типовому запиті більшість часу — це очікування бази й мережі, а гарячі внутрішні операції (функції масивів, рядків, PCRE, серіалізація) і так виконуються в C, куди JIT не дотягується; додайте до цього короткий час життя запиту, за який трасувальний JIT ледве встигає прогрітись. Виграш у рази реальний на іншому профілі навантаження: чисельні розрахунки, попіксельна обробка зображень, парсери, довгі CLI-процеси. У PHP 8.4 бекенд JIT переписали на нову проміжну репрезентацію (IR framework) — код підтримувати стало легше, порядок величини для веба не змінився.
Практичний висновок для продакшену виглядає нудно і саме тому правильно: увімкнений OPcache із memory_consumption під розмір кодової бази, max_accelerated_files із запасом над реальною кількістю .php-файлів (значення округлюється вгору до наступного числа з ряду 3907/7963/16229/32531/65407/130987), validate_timestamps=0 плюс reload на деплої, save_comments=1 — бо докблоки читає і рефлексія, і низка бібліотек. Далі — метрики з opcache_get_status() у моніторингу: hit rate нижче 95%, ненульові oom_restarts або num_cached_keys під стелею означають, що кеш регулярно скидається і застосунок половину часу живе на холодному. Preload — наступний крок, коли базове вже налаштоване й ви готові платити рестартом. JIT — останній і найчастіше зайвий: якщо профіль не показує, що понад половина часу запиту йде в самому PHP-коді, вмикати його — це додати ризик без прискорення. І, як завжди, найбільші виграші лежать поза цим списком — у запитах до бази, кількості HTTP-викликів і кешуванні відповідей.
<?php
// preload.php — виконується один раз при старті php-fpm (opcache.preload=/app/preload.php)
// php.ini прод:
// opcache.enable=1
// opcache.memory_consumption=256 ; SHM під опкоди
// opcache.interned_strings_buffer=16 ; буфер під інтерновані рядки, МБ
// opcache.max_accelerated_files=32531 ; більше за реальну кількість .php у релізі
// opcache.validate_timestamps=0 ; без stat() — деплой зобовʼязаний робити reload FPM
// opcache.save_comments=1 ; докблоки потрібні рефлексії; не вимикати
// opcache.preload=/app/preload.php
// opcache.preload_user=www-data
// opcache.jit_buffer_size=0 ; JIT вимкнено: веб-запит упирається в I/O, не в CPU
require __DIR__.'/vendor/autoload.php'; // автозавантажувач потрібен, щоб звʼязати батьків
$classmap = require __DIR__.'/vendor/composer/autoload_classmap.php';
foreach ($classmap as $class => $file) {
// виключаємо те, що не має бути звʼязаним назавжди або тягне побічні ефекти
if (str_contains($file, '/tests/') || str_contains($file, '/Database/Migrations/')) {
continue;
}
// compile_file кладе опкоди в SHM; клас, чий батько не резолвиться,
// буде пропущено з попередженням "Can't preload unlinked class"
@opcache_compile_file($file);
}
// Перевірка на проді: hit rate має бути 99%+, а рестарти — нульові.
// php -r '$s = opcache_get_status(false); var_dump(
// $s["opcache_statistics"]["opcache_hit_rate"],
// $s["opcache_statistics"]["oom_restarts"], // > 0 => замало memory_consumption
// $s["opcache_statistics"]["num_cached_keys"], // близько до max => підняти max_accelerated_files
// $s["preload_statistics"]["memory_consumption"] ?? null
// );'
Одна фраза, яка закриває питання: «Реальні гроші лежать в OPcache — прибрати компіляцію з кожного запиту й прибрати `stat()` через `validate_timestamps=0`; preload — ще кілька відсотків для фреймворку ціною рестарту на деплої; JIT для веба — статистична похибка, він для CPU-bound коду». Далі одразу додайте, чим замінюєте валідацію таймстемпів: graceful reload FPM у кінці деплою.