<? phpukraine СПІВБЕСІДИ
Пошук по платформі
CORE PHP · SENIOR

Як працюють OPcache і JIT, і що з них реально прискорює застосунок?

OPcache тримає в спільній памʼяті вже скомпільовані опкоди, прибираючи парсинг і компіляцію з кожного запиту — це і є основний виграш; preload (PHP 7.4+) додатково тримає звʼязані класи фреймворку, а JIT (PHP 8.0+, вимкнений за замовчуванням через `opcache.jit_buffer_size=0`) компілює гарячі опкоди в машинний код і майже нічого не дає типовому веб-застосунку, бо той упирається в I/O, а не в CPU.

У нас увімкнули JIT на проді, а сторінки не стали швидшими — чому?
Що саме кешує OPcache: результат виконання скрипта чи щось інше?
Ми задеплоїли новий код, а сервер віддає старий — що не так із налаштуваннями?
Що таке preload і чим він відрізняється від звичайного кешу опкодів?
OPcache JIT preload opcache.validate_timestamps PHP 8 performance PHP-FPM

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
// );'
Що PHP компілює файл при кожному запиті, і OPcache прибирає саме крок «читання файлу → лексер → парсер → компілятор», а не виконання опкодів.
Що `opcache.validate_timestamps=0` у продакшені знімає `stat()` з кожного включеного файлу, але тоді деплой зобовʼязаний завершуватись перезавантаженням FPM (або `opcache_reset()`), інакше сервер віддаватиме старий код.
Що `opcache.preload` (PHP 7.4+) виконується один раз при старті master-процесу, зберігає класи вже звʼязаними і робить їх доступними без автозавантажувача, а оновити їх можна лише рестартом — `opcache_reset()` preload не чіпає.
Що JIT — це частина OPcache, вмикається `opcache.jit_buffer_size`, і дає відчутний ефект тільки на CPU-bound коді (математика, обробка зображень, довгі CLI-процеси), а не на типовому запиті з базою й шаблонами.
Що діагностика ведеться числами з `opcache_get_status()`: hit rate, `num_cached_keys` проти `max_cached_keys`, `wasted_memory`, `oom_restarts` — а не відчуттям «наче швидше».
Казати, що OPcache «кешує результат виконання скрипта» або «кешує сторінки» — він кешує опкоди, і код усе одно виконується при кожному запиті.
Вмикати `opcache.jit=tracing` і вважати, що JIT працює: без ненульового `opcache.jit_buffer_size` він лишається вимкненим.
Ставити `opcache.validate_timestamps=0` і продовжувати деплоїти простим `git pull` без перезавантаження FPM — далі йде година пошуку «чому не застосувались зміни».
Вмикати preload на дев-машині й потім не розуміти, чому правки в класі не видно до рестарту php-fpm.
Лишати `opcache.max_accelerated_files` за замовчуванням у великому проєкті: файлів більше за ліміт, частина класів компілюється щоразу заново, а в статусі росте `misses`.
Вимикати `opcache.save_comments`, щоб «зекономити памʼять»: ламаються всі бібліотеки, які читають докблоки (Doctrine annotations, частина рефлексії в PHPUnit).
Обіцяти бізнесу «PHP 8 з JIT — це вдвічі швидше», спираючись на синтетичні бенчмарки Mandelbrot.
ПОРАДА

Одна фраза, яка закриває питання: «Реальні гроші лежать в OPcache — прибрати компіляцію з кожного запиту й прибрати `stat()` через `validate_timestamps=0`; preload — ще кілька відсотків для фреймворку ціною рестарту на деплої; JIT для веба — статистична похибка, він для CPU-bound коду». Далі одразу додайте, чим замінюєте валідацію таймстемпів: graceful reload FPM у кінці деплою.

оновлено 4 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

OPcache прибирає парсинг і компіляцію з кожного запиту — це десятки відсотків. JIT (частина того ж OPcache, вмикається `opcache.jit_buffer_size`) допомагає лише CPU-bound коду, а типовий запит чекає базу й виконує вбудовані функції на C. Preload — надбудова над OPcache, а не заміна, і без увімкненого OPcache не працює.