Назад в блог

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-уровень остаётся последним незакрытым слоем.