
Вообще говоря, я всегда делал бэкенд исключительно на PHP. Я считаю его отличным языком для бэкенда, вернее не только для бэкенда, а отличным языком. Некоторые не любят PHP за малый порог вхождения. Типа через этот малый порог вхождения входит очень много непрограммистов, которые потом пишут говнокод. Но я придерживаюсь мнения, что PHP никак не виноват в этом. Скорее наоборот. На PHP логика просыпается даже у тех, кто кроме говнокода ничего написать не может. Поэтому, в любом случае, это отличный язык программирования, который каждый год хоронят, но никак похоронить не могут. И в 2026 году он живее всех живых и показывает отличные результаты.
Вторым в списке для сравнения я решил поставить Дарт. Дело в том, что самое узкое место применения Дарта — это как раз-таки применение на серверах. Потому что в мобильной разработке он уже как стандарт, веб, фронтенд и десктоп мало-мальцевски он тоже подтягивает. Но вот на серверах пока что его используют очень мало. Одна из причин на то, что его мало используют на серверах, — это отсутствие библиотек, таких как, например, есть у PHP. Но, сейчас в эпоху LLM, это совсем не значит, что Дарт негоден на серверах. Как покажет сравнение, он очень-таки годен.
Третий кандидат для сравнения — это C++. Это его фреймворк Drogon, который в каком-то там 2014 или 2015 годах взлетел как самый быстрый фреймворк для серверов на бэкенде по скорости. Если честно, я ни разу не писал бэкенд на C++, поэтому это была первая попытка. Просто хотелось посмотреть, как C++ уничтожит двух других оппонентов со своей 100x производительностью.
Что сравнивалось
Полный существующий PHP API через PHP-FPM с OPcache и явно документированными исправлениями в одноразовых исследовательских копиях.
Независимый Dart API, скомпилированный AOT.
Независимый Drogon C++ API, сборка Release. Dart и Drogon не вызывают PHP и не перенаправляют запросы в PHP.
В нативных реализациях объявлены 127 методов app.php и реализованы семь операций ai.php; отдельно проверены HTTP upload/download, нативные push-воркеры и доставка открытым клиентам.
WebSocket-инфраструктура общая: отдельный Dart gateway для каждого варианта, обращающийся именно к соответствующему API.
Все операции замеров проходят через HTTP → nginx → реальный API → авторизацию/права → SQLite → сериализацию ответа.
Я использовал в основном то, что реально попадается на практике: отправка сообщений, хеширование, авторизация, получение блокировки записи, запись, чтение, удаление, обновление в SQLite. Код в этой публикации выложить нет возможности, возможно обновлю статью позже, добавив исходники.
Условия и воспроизводимость
Сервер: Англия/Лондон, Ubuntu 24.04, 2 vCPU AMD EPYC 9354P, 8 ГБ RAM.
PHP 8.4.22; Dart 3.13.0 linux_x64; Drogon 1.8.7; JsonCPP 1.9.5; ICU 74.2; nginx 1.30.3.
Во всех трёх API через SQL-запросы проверены SQLite 3.51.3, WAL, synchronous=FULL, foreign_keys=ON, busy_timeout=5000, page_size=4096.
У каждого варианта независимая одинаковая синтетическая база: 1000 строк тестовой сущности, 100 исходных сообщений, одинаковые роли, пользователи, индексы и разрешения. Новая копия базы для каждого числа процессов и повторения.
Все рабочие процессы исполнялись от одного пользователя.
Использован одинаковый приватный nginx, HTTP/1.1 с повторным использованием соединений, без gzip и TLS в серверном тесте.Основной прогон: 1 и 2 процесса, 3 повторения, 200 последовательных запросов на операцию в каждом повторении. В таблицах ниже по 600 исходных наблюдений на ячейку.
Основная нагрузка: 100/300/600 запросов/с, по 8 секунд; стресс: 1000/1500/2000 запросов/с, по 5 секунд, 2 процесса, 2 повторения; дополнительная проверка: 8 процессов, 1000/2000 запросов/с, по 3 секунды, 2 повторения.
Смесь: 30% история, 20% роли, 10% список чатов, 20% пагинация, 20% отправка сообщения. Сохранение контента измерялось отдельно. У отправляемых сообщений отключены внешние уведомления, но сохраняются полноценная запись, события и сигнал Redis.
Генератор посылает запланированный поток с пределом 32 одновременных запросов. Время считается от запланированного момента поступления, включая очередь генератора. Ошибки и завершившиеся запросы учитываются отдельно.
Процентили объединены из сырых выборок; это не среднее значение процентилей разных запусков. Исходные файлы и SHA256 перечислены в JSON-сводках.
Ограничение стенда: генератор нагрузки, прокси и рабочий проект используют те же 2 vCPU. Например, при 600 запросах/с генератор тратил около 0,60 мс CPU на запрос. Его CPU и память измерены отдельно. Поэтому значения на перегрузке — наблюдаемая производительность всей этой конфигурации, а не универсальный предел языка или чистый максимум API на выделенном железе.
Тёплые запросы, один рабочий процесс
В каждой ячейке p50 / p95, мс. Меньше — лучше.
Операция | PHP-FPM | Dart AOT | Drogon C++ |
|---|---|---|---|
Снимок ролей и разрешений | 2.17 / 2.95 | 1.01 / 2.02 | 0.96 / 1.45 |
История: 80 сообщений | 2.72 / 4.16 | 3.33 / 5.12 | 3.76 / 4.64 |
Список чатов | 2.13 / 2.91 | 0.93 / 1.87 | 0.91 / 1.42 |
Пагинация: 40 строк | 2.18 / 3.17 | 1.20 / 2.33 | 1.22 / 1.59 |
Сохранение сообщения | 4.99 / 7.66 | 2.14 / 4.36 | 1.85 / 3.25 |
Сохранение документа | 2.35 / 3.60 | 0.97 / 1.86 | 0.98 / 1.36 |
Для отправки сообщения Drogon уменьшил медиану относительно PHP примерно на 63%, Dart — на 57%. Для истории PHP опередил обе текущие нативные реализации. Это разница конкретного кода: наличие C++ само по себе не гарантирует более быстрый результат.
Смешанная нагрузка 600 запросов/с, два процесса
API | Обработано, запросов/с | p50, мс | p95, мс | p99, мс | CPU API, мс/запрос | Память PSS, МиБ |
|---|---|---|---|---|---|---|
PHP-FPM | 524.38 | 236.76 | 879.42 | 1 207.94 | 1.97 | 30.91 |
Dart AOT | 599.79 | 3.42 | 31.76 | 167.65 | 1.14 | 31.08 |
Drogon C++ | 599.92 | 3.59 | 8.19 | 11.99 | 1.32 | 12.17 |
Dart и Drogon выдержали входящий поток 600 запросов/с в этом тесте; PHP с двумя процессами не успевал. Ноль ошибок у PHP здесь означает, что очередь в конце дождалась обработки, а не что задержка была приемлемой. При одном процессе PHP обработал около 377 запросов/с на таком же входящем потоке; p95 достигал 4,58 секунды.
PSS — сумма пропорционально разделённой памяти процессов API, включая мастер PHP-FPM. Она не считает общие страницы целиком по нескольку раз, как сумма RSS. Это наблюдение после серии, не непрерывно измеренный пик. Общий nginx, генератор, Redis, WebSocket gateway и push-воркеры в этой колонке не смешаны с памятью API.
Перегрузка, два процесса:
Входящий поток | API | Обработано, запросов/с | p50, мс | p95, мс | PSS, МиБ |
|---|---|---|---|---|---|
1000 | PHP-FPM | 560.94 | 1 971.21 | 3 853.38 | 31.37 |
1000 | Dart AOT | 996.98 | 10.86 | 107.44 | 33.50 |
1000 | Drogon C++ | 973.27 | 54.82 | 128.49 | 12.64 |
1500 | PHP-FPM | 531.80 | 4 715.65 | 8 617.77 | 31.36 |
1500 | Dart AOT | 1 051.33 | 1 090.88 | 2 045.36 | 34.59 |
1500 | Drogon C++ | 922.35 | 1 534.06 | 2 934.37 | 12.70 |
2000 | PHP-FPM | 509.15 | 6 633.42 | 13 945.13 | 31.37 |
2000 | Dart AOT | 1 096.23 | 2 052.18 | 3 951.29 | 36.22 |
2000 | Drogon C++ | 919.18 | 2 812.01 | 5 562.40 | 12.73 |
При 2000 входящих запросах/с все варианты перегружены. Dart обработал примерно в 2,15 раза больше запросов, чем PHP с теми же двумя процессами; Drogon — примерно в 1,81 раза больше. Это не режим, который стоит принимать за нормальную эксплуатацию: задержки уже измеряются секундами, в реальных проектах при таком - нужно думать над оптимизацией или апгрейдить железо.
Проверка пула из восьми процессов
API | Обработано при входящих 1000/с | Обработано при входящих 2000/с | PSS при 2000/с, МиБ |
|---|---|---|---|
PHP-FPM | 721.33 | 709.72 | 46.77 |
Dart AOT | 931.36 | 927.14 | 117.60 |
Drogon C++ | 950.59 | 798.98 | 34.08 |
Больше процессов помогло PHP: на перегрузке получено около 710–721 запросов/с вместо 509–561 при двух процессах. Поэтому было бы неправильно называть 509 запросов/с пределом PHP. Dart и Drogon от восьми процессов на этих двух vCPU не выиграли; память выросла. Среди проверенных конфигураций лучше выглядит небольшой пул нативных процессов и более широкий PHP-FPM-пул. Для назначения production SLO нужны более длительные прогоны с внешним генератором и реальным распределением данных.
Разбор времени отправки сообщения
Медианы внутренних таймеров при одном процессе, мс:
Этап | PHP-FPM | Dart AOT | Drogon C++ |
|---|---|---|---|
Авторизация | 0.23 | 0.09 | 0.10 |
Получение блокировки записи | 0.01 | 0.01 | 0.01 |
SQL-записи | 0.24 | 0.10 | 0.11 |
Коммит | 1.54 | 0.92 | 0.86 |
Сериализация ответа | 0.00 | 0.00 | 0.01 |
Внутренний total | 3.30 | 1.33 | 1.20 |
Таймеры вложены друг в друга, и их медианы нельзя складывать для получения общей медианы. Внешнее время HTTP включает также прокси, планирование процессов и передачу ответа. В PHP отправка уведомления Redis измерялась отдельно: медиана около 0,25 мс. В нативных API публикация асинхронная.
Коммит остаётся заметной частью отправки: около 0,86–1,54 мс в этих сериях. Переписывание языка не убирает ожидание надёжной записи SQLite. На чтении истории у Dart/Drogon дороже текущая обработка и сериализация: внутренний dispatch примерно 1,46/2,00 мс против 0,64 у PHP; encode примерно 0,42/0,37 против 0,04 мс. Именно этот путь заслуживает следующего профилирования нативных реализаций.
Также я отдельно тестил быстродействие модуля чата, хотелось проследить за каждым промежутком времени в процессах от нажатия кнопки отправки сообщения до получения в интерфейсе галочки "отправлено". Об этом ниже.
От нажатия «отправить» до кадра с галочкой
По 30 измеренных сообщений на вариант после трёх прогревочных; отправка через настоящий WSS, интервал между сценариями 400 мс после подтверждения. Все 90 сообщений подтверждены; ошибок Flutter в журнале нет. Время отсчитывается внутри обработчика, поэтому запуск CLI и доставка команды автоматизации не включены.
Backend | До кадра с галочкой p50, мс | p95, мс | Диапазон, мс | WSS внутри клиента p50, мс |
|---|---|---|---|---|
PHP-FPM | 117.50 | 126.22 | 109.10–126.93 | 105.71 |
Dart AOT | 120.70 | 136.97 | 108.17–138.27 | 103.93 |
Drogon C++ | 117.20 | 123.44 | 109.22–125.66 | 103.05 |
В этом коротком живом сценарии переход с PHP на Dart/Drogon не дал заметного ускорения галочки. Небольшие различия медиан сравнимы с сетевыми колебаниями и ожиданием следующего кадра. У Dart медиана кадра выше PHP, хотя медиана клиентского WSS ниже.
Тёплые внешние HTTPS запросы
Финальный сетевой прогон: 40 измерений на каждую из шести операций для каждого API; всего 720 запросов. По два прогревочных запроса исключены. Значения p50 / p95, мс:
Операция | PHP-FPM | Dart AOT | Drogon C++ |
|---|---|---|---|
Роли | 102.97 / 105.42 | 101.66 / 103.81 | 101.89 / 102.73 |
История 80 сообщений | 103.72 / 117.38 | 105.81 / 111.16 | 103.93 / 105.12 |
Список чатов | 102.83 / 103.97 | 101.82 / 103.61 | 101.78 / 103.31 |
Пагинация 40 строк | 102.97 / 104.78 | 101.66 / 103.34 | 102.90 / 106.42 |
Отправка сообщения | 104.47 / 105.36 | 102.60 / 104.57 | 103.10 / 104.40 |
Сохранение контента | 102.54 / 103.08 | 101.70 / 102.72 | 101.81 / 103.00 |
Подтверждение и доставка второму открытому клиенту по WebSocket
Отдельно 60 сообщений на вариант после двух прогревочных; ACK — подтверждение отправителю; доставка — получение соответствующего сообщения вторым сокетом. Это транспортные измерения, без Flutter. Ошибок и таймаутов в завершённом прогоне нет.
Backend | ACK p50, мс | ACK p95, мс | Доставка p50, мс | Доставка p95, мс | Доставка p99, мс |
|---|---|---|---|---|---|
PHP-FPM | 103.75 | 105.96 | 105.19 | 106.86 | 112.38 |
Dart AOT | 108.74 | 111.58 | 109.24 | 111.68 | 120.81 |
Drogon C++ | 102.47 | 103.84 | 103.07 | 104.17 | 105.77 |
Установка TCP-соединения: медиана 99.60 мс, 12 проб. TCP вместе с TLS: 203.18 мс, 6 проб. Это два разных измерения; второе включает первое. Все тёплые результаты выше используют уже установленные соединения. TCP connect показывает масштаб сетевой составляющей около 100 мс, т е по сути большая часть времени уходила на сетевой транспорт, а не на обработку на бэкенде, поэтому особого толку от замены PHP на более высокопроизводительные языки на малых нагрузках - просто нет. Тут вспомнились Дуровы, Цукерберги, киткаты и HPHPHPHP.
Вывод
Для простой настройки сервера и простого поиска специалиста, который сможет быстро настроить бэкенд, при этом довольно близкого уровня производительности - в лидерах, конечно, PHP. Для высокой пропускной способности текущей смешанной нагрузки предпочтительнее Dart AOT. Dart оказался тем самым серединным путем по скорости близок к Drogon, по потреблению памяти к PHP, поэтому свой выбор остановлю на нем. Для минимальной памяти и максимальных нагрузкок — Drogon C++. Выбор между заменой стека и покупкой железа лежит на пользователе, но при текущих ценах на память, C++ может реально сэкономить бюджет.
При сохранении PHP существенное улучшение под этой нагрузкой уже даёт правильно выбранный пул PHP-FPM, хотя в проверенных конфигурациях он уступает нативным вариантам по смешанной пропускной способности. Переводить всё на C++ только из предположения «C++ всегда быстрее» эти данные не поддерживают. 100x я не получил и 10x тоже (возможно, я не знаю каких-то секретов оптимизации C++ и попрошу тех, кто знает - написать подобную статью, с удовольствием прочту), это еще раз показывает, что современные языки программирования в большинстве случаев не настолько тормознутые и выбор C++ только ради увеличения производительности - далеко не всегда себя оправдывает.

