Слухи о медленности PHP родились не на пустом месте. Классический PHP-процесс двадцать лет компилировал скрипт заново на каждый запрос, ничего не хранил между обращениями и выполнял бутстрап фреймворка десятки раз в секунду. Поколение PHP 7.0, вышедшее в декабре 2015 года, переписало движок и удвоило скорость против версии 5.6. PHP 7.4, релиз которого датирован 28 ноября 2019 года, добавил типизированные свойства и предзагрузку, но 28 ноября 2022 года ветка умерла: последним патчем остался 7.4.33, и с тех пор дыры в ней закрывать некому. PHP 8.3 из релиза 23 ноября 2023 года получает заплатки безопасности до 31 декабря 2027 года, а рядом уже живут ветки 8.4 и 8.5. Между этими строками и живёт практический вопрос: что даёт переход с 7.4 на 8.3, кроме чувства современности. Ответ честнее, чем кажется: прирост зависит от профиля приложения и расходится от трёх процентов до двадцати с лишним. Разберём, откуда берётся такая разница и что типизированный код даёт помимо скорости.

Откуда взялась репутация медленного PHP и как её меняли

Историческая медленность складывалась из трёх слагаемых. Первое, интерпретатор без кэша байткода компилировал каждый файл при каждом запросе, и большой фреймворк тратил на это заметную долю времени. Второе, архитектура без общего состояния означала, что между запросами не живёт ничего: соединение с базой, дерево классов, конфигурация собирались заново. Третье, кэш байткода долгие годы оставался факультативом, хотя в дистрибутив он встроен с ветки 5.5, и на дешёвых хостингах расширение часто просто не включалось. Следы той эпохи живы до сих пор: в панелях управления хостингом встречаются галочки выбора PHP 5.6, и за ними стоят реальные сайты.

Поколение 7.0 в декабре 2015 года поменяло правила: значения стали компактнее, хеш-таблицы экономнее, появилось абстрактное синтаксическое дерево, и по официальным замерам производительность выросла примерно вдвое против 5.6. Ветка 7.4, вышедшая 28 ноября 2019 года, добавила типизированные свойства, стрелочные функции и предзагрузку opcache preloading, которая впервые позволила держать классы фреймворка в памяти между запросами. Именно эта ветка закрыла линейку 7, и именно она до сих пор встречается на живых серверах, хотя формально мертва почти четыре года.

Реальные ускорители выполнения opcache preloading и JIT

Оптимизатор opcache превращает скрипты в байткод один раз и хранит его в разделяемой памяти процессов php-fpm. Продакшен-стандарт выглядит так:

opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=50000
opcache.validate_timestamps=0

Ноль в последней строке запрещает проверять время изменения файлов на каждом запросе, экономя системные вызовы, и потому релиз сопровождается перезапуском php-fpm. Буфер интернированных строк собирает повторяющиеся литералы в общую память, на больших проектах это заметные мегабайты. Предзагрузка из PHP 7.4 идёт дальше: файл preload компилируется один раз при старте мастера, разрешает циклические зависимости и оставляет перечисленные классы в памяти навсегда:

opcache.preload=/var/www/app/preload.php
opcache.preload_user=www-data
// preload.php: компилируем сущности один раз при старте php-fpm
foreach (glob(__DIR__.'/src/*/Entity/*.php') as $file) {
    opcache_compile_file($file);
}

JIT из PHP 8.0 добавляет последний слой: горячие трассы байткода компилируются в машинный код прямо в памяти воркера. Включается режим явно, ненулевым буфером:

opcache.jit=tracing
opcache.jit_buffer_size=64M

Проверить фактические значения в бою помогает одна строка:

php -i | grep -E "opcache.jit|opcache.preload"

Здесь нужна трезвость. На счётных синтетических тестах JIT даёт кратный прирост, но реальный веб-запрос упирается в базу, сеть и ожидания, поэтому в типичном приложении выигрыш измеряется единицами процентов. JIT лечит тяжёлую математику, обработку изображений и парсеры, а не медленные запросы к MySQL.

Здоровье кэша удобно наблюдать в цифрах: доля попаданий показывает, сколько запросов обошлось без компиляции:

$stats = opcache_get_status()['opcache_statistics'];
echo $stats['opcache_hit_rate'];

Значения выше 99 процентов считаются нормой продакшена, падение означает нехватку памяти или лишние файлы в дереве проекта.

Честные цифры при переходе с 7.4 на 8.3 на идентичном стенде

Замеры 2026 года на одной конфигурации, nginx и php-fpm, opcache включён, JIT выключен, память скрипта 128 МБ, дают наглядную картину. WordPress 6.8 на PHP 7.4 выдаёт 139.06 запроса в секунду, на 8.3 уже 142.75, а на 8.5 доходит до 148.30. Прирост перехода с 7.4 на 8.3 около трёх процентов, на уровне погрешности мониторинга. Линейка 8.2, 8.3 и 8.4 держится в коридоре от 142 до 148 запросов в секунду, скачков внутри поколения нет. Зато WooCommerce 10.2.2 на том же стенде поднимается с 44.20 запроса в секунду на 7.4 до 53.74 на 8.3, это плюс почти двадцать два процента без изменения кода магазина.

Разница объясняется профилем. Простая страница упирается в кэш и выборку из базы, движку оставался узкий зазор. Тяжёлый стек с тысячами классов, событиями и плагинами получает от каждой новой ветки движка свою долю. Отдельная честность в том, что прыжок WooCommerce до 71.02 запроса в секунду на 8.5 объясняется уменьшившимся размером ответа, а не внезапным ускорением языка: цифры всегда читают вместе с условиями замера. Вывод практичный: обновление не замедляет никого, а ускоряет тем заметнее, чем толще framework-слой вокруг бизнес-логики. Для внутренней панели на самописном коде прирост скромный, для магазина на тяжёлом фреймворке он ощущается в счетах за железо.

Типизированный код PHP 8.3 что он даёт движку и команде

Синтаксис типов развивался линейно: свойства с типами пришли в 7.4, продвижение аргументов конструктора и выражение match в 8.0, перечисления и readonly-свойства в 8.1, readonly-классы в 8.2. Версия 8.3 закрыла контур типизированными константами классов и функцией json_validate. Константы тоже получили право на тип, и опечатка в значении ловится сразу:

CODE6 Современный код выглядит иначе, чем наследие 7.4:

final class Order
{
    public function __construct(
        public readonly int $id,
        public readonly \DateTimeImmutable $createdAt,
    ) {
    }
}

enum Status: string
{
    case New = 'new';
    case Paid = 'paid';
}

Ветвление без каскада switch тоже стало выражением, а сравнение тарифов умещается в четыре строки:

declare(strict_types=1);

$rate = match (true) {
    $total >= 50000 => 0.15,
    $total >= 10000 => 0.10,
    default => 0.0,
};

Проверка входящего JSON перед декодированием перестала требовать try-каскадов:

if (json_validate($payload)) {
    $data = json_decode($payload, true, flags: JSON_THROW_ON_ERROR);
}

Типы не разгоняют движок магическим образом, их вклад в скорость косвенный. Настоящая сила в предсказуемости: движок 8.0 перестал считать ноль равным произвольной строке, неявные приведения сужаются от релиза к релизу, а статический анализатор PHPStan на максимальном уровне ловит несоответствия ещё до запуска тестов. Типизированная кодовая база рефакторится без страха и покрывается тестами быстрее, а каждый исправленный до продакшена баг экономит ночи дежурному. Историческая медленность лечится opcache и JIT, историческая хаотичность лечится только типами.

Миграция живого проекта с 7.4 на 8.3 по шагам

Переезд готовится заранее и выполняется предсказуемо:
1. поднять окружение на образе php:8.3-fpm в контейнере и прогнать тесты PHPUnit на текущем коде без изменений;
2. закрепить требование к версии в манифесте зависимостей и обновить пакеты;
3. прогнать статический анализ PHPStan и закрыть предупреждения о динамических свойствах и старых сравнениях;
4. собрать конфигурацию opcache с предзагрузкой и JIT, снять метрики стенда до и после переключения живого трафика.

Манифест зависимостей фиксирует версию жёстко, чтобы окружение не расползалось:

{
    "require": {
        "php": "^8.3",
        "ext-json": "*"
    }
}

Узнать, какие пакеты мешают переходу, помогает встроенная команда:

composer why-not php 8.3

На время переезда матрица CI гоняет тесты на обеих ветках, чтобы регрессии ловились до слияния:

strategy:
  matrix:
    php: ["7.4", "8.3"]

Образ контейнера ставит новую ветку одной строкой:

FROM php:8.3-fpm
COPY docker/opcache.ini /usr/local/etc/php/conf.d/opcache.ini

Анализатор гоняется одной командой и сразу показывает расстояние до идеала:

vendor/bin/phpstan analyse src --level=max

Классические грабли переезда известны давно: неявные сравнения чисел со строками после 8.0, динамические свойства классов после 8.2, изменения уровней предупреждений. Тестовый бегун обновляется вместе с веткой: PHPUnit 10 требует PHP 8.1, PHPUnit 11 требует 8.2, поэтому старый комплект проверок на 7.4 в любом случае придётся поднимать. Грабли переезда известны давно: неявные сравнения чисел со строками после 8.0, динамические свойства классов после 8.2, изменения уровней предупреждений. Всё это ловится тестами и анализатором за неделю спокойной работы, а не годами по частям на живых клиентах.

Настройка php-fpm opcache и JIT в продакшене под PHP 8.3

Помимо кэша байткода, продакшен настраивает пул воркеров. Типовые значения для среднего проекта:

pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 4
pm.max_spare_servers = 8

Двадцать воркеров умещают примерно двадцать одновременных тяжёлых запросов, дальше очередь растет на уровне nginx. Считают от памяти: один воркер съедает от 30 до 60 МБ, значит пул из двадцати просит около гигабайта, и потолок pm.max_children подбирают по железу, а не по привычке. Для приложений с тяжёлым framework-слоем список предзагрузки удобно не писать руками: Symfony генерирует его автоматически при прогреве кэша, остаётся указать файл в конфигурации opcache. После каждого релиза с выключенной проверкой временных меток обязателен перезапуск php-fpm, иначе процессы продолжат исполнять старый байткод до полной выгрузки.

Отдельный практический совет: включайте JIT только с профилем нагрузки в руках. Счётный код вроде ресайза изображений или расчёта тарифов получает от трасс кратный выигрыш, простые CRUD-страницы ничего не замечают, а буфер отъедает память у воркеров. Правило простое: сначала opcache и предзагрузка для всех, потом JIT под конкретную нагрузку.

Реальная мотивация обновления безопасность и экосистема важнее процентов

Четыре года без единой заплатки стоят дороже любых процентов производительности. С 28 ноября 2022 года для 7.4 не выпущено ни одного исправления, а найденные позже дыры остаются дырами навсегда. Ветка 8.3 получает патчи безопасности до 31 декабря 2027 года, активная поддержка закончилась в конце 2025 года, и один факт безопасности перевешивает всю арифметику запросов в секунду. К нему добавляется давление экосистемы: свежие мажоры Symfony и Laravel требуют PHP 8.2 и выше, новые версии пакетов в Composer перестают объявлять поддержку старых веток, и проект на 7.4 постепенно замыкается в кольцо из устаревших зависимостей. Каденция языка проста и предсказуема: ветка выходит каждый ноябрь, два года получает исправления ошибок и ещё два года только заплатки безопасности. Планировать переезд по календарю веток проще, чем после инцидента.

Честный итог выглядит так. Прирост скорости перехода с 7.4 на 8.3 реален, но скромен на лёгких страницах и достигает двадцати с лишним процентов на тяжёлых стеках. Настоящая цена старой ветки не в медленных запросах, а в дырах без заплаток и потолке экосистемы, который опускается с каждым кварталом. Типизированный код 8.3 в этой истории не ускоритель, а страховка: он не даёт скорости бесплатно, зато делает кодовую базу понятной для людей и инструментов. Инженер, который переносит проект с 7.4 на 8.3, покупает не проценты, а годы спокойствия, и это самая выгодная сделка в его году.