Nginx перед Bitrix: пять строк, которые убирают 600 мс TTFB
У большинства Bitrix-сайтов TTFB выше 700 мс. Не потому что PHP медленный. Потому что Nginx-конфиг лежит нетронутым с того дня, когда хостер его положил — часто в 2016–2017 году.
Мы проверяем это в первый час на каждом новом проекте. И почти всегда находим одно и то же.
TTFB > 700 мс на Bitrix — не судьба
Для контекста: TTFB — это время от отправки запроса до первого байта ответа. Google считает хорошим показателем < 600 мс. Всё что выше — минус к Core Web Vitals, минус к ранжированию.
На одном проекте со 28k SKU мы видели медианный TTFB 1,1 с. При том что PHP-FPM отвечал за 480 мс. Разница в 620 мс жила внутри Nginx. Не в коде, не в базе данных. В конфиге, который никто не трогал три года.
Когда мы это исправили, медианный TTFB упал до 480 мс. LCP улучшился с 2,9 с до 2,1 с. Без единого изменения в PHP-коде.
Что происходит между браузером и PHP
Стандартная схема на Bitrix: браузер → Nginx → PHP-FPM → MySQL → ответ назад.
Nginx здесь работает как reverse proxy. Он принимает соединение, передаёт запрос PHP-FPM по FastCGI-протоколу, получает ответ, возвращает браузеру.
Проблема дефолтного конфига — в том, что каждый запрос неизбежно будит PHP. Каталожная страница, которую вы открываете в 15-й раз, обрабатывается заново. Статический файл запрашивает файловую систему при каждом обращении. Соединение к FPM-воркеру открывается и закрывается при каждом запросе.
Это работает. Но это медленно.
Пять параметров, которые это меняют.
fastcgi_cache: кэшировать страницы до PHP
Самый весомый параметр. fastcgi_cache позволяет Nginx хранить готовый HTML-ответ от PHP и отдавать его следующим запросам без обращения к PHP-FPM вообще.
На пиках нагрузки это снижает количество запросов к PHP в 3–5 раз. Для каталожных страниц, которые меняются редко — идеально.
Базовая конфигурация:
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=bitrix_cache:64m inactive=10m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_valid 200 10m;
fastcgi_cache_valid 404 1m;
fastcgi_cache bitrix_cache;
fastcgi_cache_bypass $no_cache;
fastcgi_no_cache $no_cache;
$no_cache — это переменная, которую нужно собрать из условий. О них дальше.
Важный момент: при fastcgi_cache_valid 200 10m каталожные страницы живут в кэше 10 минут. Если товар обновился — кэш устареет. Для большинства каталогов это приемлемо. Для страниц с ценами в реальном времени — нет.
open_file_cache и буферы: мелочи, которые складываются
open_file_cache убирает повторные syscall stat() для статических файлов — CSS, JS, изображений.
open_file_cache max=1000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
Без этого каждый запрос статики проверяет файловую систему. С ним — Nginx кэширует дескрипторы. На сайтах с 200–400 уникальными статическими ресурсами разница заметна не в TTFB, а в RPS при нагрузке.
Буферы — отдельная история. Дефолтный fastcgi_buffer_size 4k и fastcgi_buffers 8 4k рассчитаны на маленькие ответы. Bitrix-страница с каталогом легко весит 50–80 кБ. Если ответ не помещается в буфер, Nginx пишет его на диск и читает оттуда — дополнительные 30–60 мс к каждому запросу.
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
fastcgi_busy_buffers_size 64k;
keepalive к PHP-FPM upstream: TCP overhead незаметен, пока не увидишь profiler
По умолчанию Nginx открывает новое TCP-соединение к PHP-FPM для каждого запроса. На высоконагруженных проектах это создаёт overhead в 5–15 мс на запрос — мелочь для одного, накапливается при 500+ RPS.
upstream php_fpm {
server 127.0.0.1:9000;
keepalive 32;
}
И в location-блоке:
fastcgi_keep_conn on;
Это позволяет Nginx переиспользовать соединения к PHP-FPM вместо того, чтобы открывать новые. При локальном PHP-FPM (127.0.0.1) выигрыш скромный. При вынесенном PHP-сервере — ощутимее.
Bitrix-специфика: что обязательно исключить из кэша
Без правильных исключений fastcgi_cache сломает авторизацию, корзину и все динамические запросы.
Переменная $no_cache, которую я упомянул выше, собирается так:
map $request_uri $no_cache_uri {
default 0;
~*/bitrix/ 1;
~*/local/ 1;
~*/cart/ 1;
~*/personal/ 1;
~*/order/ 1;
~*/checkout/ 1;
}
map $http_cookie $no_cache_cookie {
default 0;
~*BITRIX_SM_LOGIN 1;
~*PHPSESSID 1;
}
set $no_cache 0;
if ($request_method = POST) { set $no_cache 1; }
if ($no_cache_uri) { set $no_cache 1; }
if ($no_cache_cookie) { set $no_cache 1; }
if ($http_x_requested_with = "XMLHttpRequest") { set $no_cache 1; }
Логика: если пользователь авторизован (есть BITRIX_SM_LOGIN в cookie), его запросы идут напрямую в PHP. Корзина, личный кабинет, AJAX-запросы — минуют кэш всегда.
После настройки проверьте вручную: авторизованный пользователь и анонимный. Корзина, личный кабинет, AJAX-запросы — всё должно работать у обоих.
Результат и что не работает
На проекте со 28k SKU после внедрения всех пяти параметров:
- Медианный TTFB: 1,1 с → 480 мс
- Пиковый TTFB: 1,8 с → 0,9 с
- LCP: 2,9 с → 2,1 с
- Нагрузка на PHP-FPM при пиках: снизилась на 40% (кэш отрабатывает каталожные запросы)
Что не работает или усложняет картину:
Если фронт уже использует Next.js ISR, fastcgi_cache создаёт второй кэш-слой с независимым TTL. Инвалидирование становится непредсказуемым. Выбирайте что-то одно.
keepalive к PHP-FPM не поможет, если pool настроен с недостаточным pm.max_children (статья про настройку FPM). Запросы будут стоять в очереди независимо от keepalive. Порядок имеет значение: сначала FPM, потом Nginx.
open_file_cache при деплоях каждые несколько минут выключайте или снижайте open_file_cache_valid до 5–10 секунд. Иначе Nginx отдаёт старую версию статики.
Час настройки, результат виден в WebPageTest сразу. Если PHP-FPM pool уже настроен (php-fpm-pool-bitrix-highload) и базовые PHP-проблемы закрыты (bitrix-not-slow-its-you) — Nginx-уровень остаётся последним незакрытым слоем.