Nginx FastCGI Cache для WordPress: Ускорение в 10 раз без плагинов

Знаете, что меня всегда удивляет? Люди ставят Redis, OPcache, сжимают картинки, минифицируют CSS. А потом открывают PageSpeed Insights и видят 60 баллов. Спрашиваю: «А кэш страниц у вас есть?» Отвечают: «Да, WP Rocket стоит». Смотрю на TTFB — 800 миллисекунд. И всё становится понятно.

Плагины кэширования — это хорошо. Но это как нанять курьера, который бегает на склад, берёт готовую посылку и несёт клиенту. А FastCGI Cache — это когда посылка лежит прямо на столе у курьера. Он даже вставать не должен.

Сегодня настроим кэш на уровне Nginx. Без единого плагина. TTFB упадёт с 800 мс до 10-20 мс. Страницы будут отдаваться в 10-50 раз быстрее. И сервер при этом будет есть заметно меньше ресурсов, чем с WP Rocket или W3 Total Cache.

Что такое FastCGI Cache и почему он круче плагинов

Давайте разберёмся, как работает обычный плагин кэширования. Когда пользователь заходит на сайт, плагин проверяет: «А есть ли у меня готовый HTML для этой страницы?» Если есть — отдаёт. Если нет — запускает WordPress, тот делает 200 запросов к базе, генерирует HTML, плагин сохраняет его в файл, и только потом отдаёт пользователю.

Проблема? Плагин работает внутри PHP. А PHP — это медленный слой. Даже если HTML готов, PHP должен загрузиться, прочитать файл с диска, отдать его. Это 100-300 мс на каждую страницу.

FastCGI Cache работает до PHP. Nginx получает запрос, смотрит в свою память: «А есть ли у меня готовый ответ?» Если есть — отдаёт сразу. PHP даже не просыпается. MySQL даже не получает запрос. Это уровень веб-сервера, а не приложения.

Сравнение с плагинами кэширования:

Параметр Плагин (WP Rocket, W3TC) FastCGI Cache (Nginx)
Где работает Внутри PHP На уровне веб-сервера
Где хранится кэш Файлы на диске Оперативная память (или диск)
TTFB 100-300 мс 5-20 мс
Нагрузка на CPU Средняя (PHP всё равно грузится) Минимальная (PHP не участвует)
Настройка Простая (кнопка в админке) Требует доступа к Nginx
Совместимость с WooCommerce Хорошая (плагины умеют исключения) Отличная (настраивается вручную)
Стоимость $50-200/год (премиум-плагины) Бесплатно (встроен в Nginx)

Разница колоссальная. FastCGI Cache — это как перейти с велосипеда на спорткар. Оба везут из точки А в точку Б. Но ощущения совсем другие.

Шаг 1: Проверяем, что Nginx поддерживает FastCGI Cache

FastCGI Cache встроен в Nginx по умолчанию. Но нужно убедиться, что модуль ngx_http_fastcgi_module скомпилирован. Проверяем:

nginx -V 2>&1 | grep -o 'http_fastcgi_module'

Если команда вернула http_fastcgi_module — всё отлично. Если пусто — нужно перекомпилировать Nginx с этим модулем. Но в 99% случаев (особенно если Nginx установлен из стандартных репозиториев) модуль уже есть.

Шаг 2: Создаём зону кэша в nginx.conf

Открываем главный конфиг Nginx (/etc/nginx/nginx.conf) и добавляем в секцию http:

http {
    # ... существующие настройки ...

    # Зона кэша FastCGI
    fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=WORDPRESS:100m inactive=60m use_temp_path=off;

    # Ключ кэширования (что отличает один запрос от другого)
    fastcgi_cache_key "$scheme$request_method$host$request_uri";

    # Формат лога, который понадобится для мониторинга hit rate (Шаг 6) —
    # стандартный формат лога Nginx НЕ включает статус кэша, поэтому без
    # этой строки скрипт мониторинга из Шага 6 всегда будет показывать 0%.
    log_format cache_log '$remote_addr - [$time_local] "$request" $status '
                          'upstream_cache_status: $upstream_cache_status';

    # ... остальной конфиг ...
}

Разберём, что здесь происходит:

  • /var/cache/nginx — папка, где будет храниться кэш. Если хотите держать всё в RAM — можно использовать tmpfs (об этом ниже)
  • levels=1:2 — структура папок. Создаётся двухуровневая иерархия, чтобы не было папок с миллионами файлов
  • keys_zone=WORDPRESS:100m — имя зоны кэша и размер памяти для ключей. По собственным рекомендациям Nginx, 100 МБ хватает примерно на 800 000 ключей
  • inactive=60m — если к странице не обращались 60 минут, она удаляется из кэша
  • use_temp_path=off — важная опция! Без неё Nginx будет писать кэш во временную папку, а потом переносить. Это замедляет работу
  • log_format cache_log — именованный формат лога с полем $upstream_cache_status. В Шаге 3 подключим его через access_log, чтобы скрипту мониторинга из Шага 6 было что грепать

Хранение кэша в RAM

Если у вас достаточно оперативной памяти, можно держать кэш целиком в tmpfs вместо диска. Тут стоит сделать оговорку: на современных SSD/NVMe Linux и так держит горячие файлы в page cache в RAM, поэтому практическая разница от tmpfs часто небольшая. Реально tmpfs выручает на медленных дисках (HDD, некоторые бюджетные VPS с сетевым хранилищем) или когда нужна жёсткая гарантия, что чтение кэша вообще не трогает диск.

Добавляем в /etc/fstab:

tmpfs /var/cache/nginx tmpfs defaults,size=1G 0 0

Перемонтируем:

sudo mount -a
sudo mkdir -p /var/cache/nginx
sudo chown www-data:www-data /var/cache/nginx

Теперь кэш хранится в RAM. При перезагрузке сервера он очистится, но это не страшно — Nginx быстро его пересоздаст.

Шаг 3: Настраиваем кэш для сайта WordPress

Открываем конфиг вашего сайта (/etc/nginx/sites-available/custom-code.ru) и добавляем правила кэширования. Вот полный рабочий пример:

server {
    listen 80;
    server_name custom-code.ru www.custom-code.ru;
    return 301 https://$server_name$request_uri;
}

server {
    # Современный синтаксис (Nginx 1.25.1+) отделяет http2 от listen:
    listen 443 ssl;
    http2 on;
    # На старых версиях Nginx используйте: listen 443 ssl http2;
    server_name custom-code.ru www.custom-code.ru;
    root /var/www/wordpress;
    index index.php index.html;

    # SSL-сертификаты
    ssl_certificate /etc/letsencrypt/live/custom-code.ru/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/custom-code.ru/privkey.pem;

    # Используем формат cache_log из nginx.conf, чтобы у скрипта
    # мониторинга из Шага 6 были данные для подсчёта hit rate.
    access_log /var/log/nginx/access.log cache_log;

    # === Настройки FastCGI Cache ===
    set $skip_cache 0;
    set $skip_cache_reason "";

    # Не кэшируем админку
    if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php") {
        set $skip_cache 1;
        set $skip_cache_reason "admin";
    }

    # Не кэшируем, если есть куки WordPress
    if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
        set $skip_cache 1;
        set $skip_cache_reason "logged_in";
    }

    # Не кэшируем любой URL с параметрами (поиск, пагинация, UTM-метки и т.д.)
    # Оговорка: это сознательно консервативное правило. Оно же режет кэш
    # для безобидных параметров типа ?utm_source=..., что может снижать
    # hit rate на трафике из рекламы/рассылок. Если это критично для вашего
    # трафика, можно нормализовать или отфильтровывать безопасные параметры
    # вместо полного отключения кэша при любой query string.
    if ($query_string != "") {
        set $skip_cache 1;
        set $skip_cache_reason "query_string";
    }

    # Не кэшируем POST-запросы
    if ($request_method = POST) {
        set $skip_cache 1;
        set $skip_cache_reason "post";
    }

    # === Исключения для WooCommerce ===
    if ($request_uri ~* "/cart/|/checkout/|/my-account/|/wc-api/") {
        set $skip_cache 1;
        set $skip_cache_reason "woocommerce";
    }

    # Если в корзине есть товары — не кэшируем
    if ($http_cookie ~* "woocommerce_cart_hash|woocommerce_items_in_cart") {
        set $skip_cache 1;
        set $skip_cache_reason "cart_not_empty";
    }

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        try_files $uri =404;

        # === FastCGI Cache ===
        fastcgi_cache WORDPRESS;
        fastcgi_cache_valid 200 60m;
        fastcgi_cache_valid 404 1m;
        fastcgi_cache_bypass $skip_cache;
        fastcgi_no_cache $skip_cache;
        fastcgi_cache_use_stale error timeout updating invalid_header http_500;
        fastcgi_cache_lock on;
        fastcgi_cache_lock_timeout 5s;

        # Заголовки для отладки
        add_header X-FastCGI-Cache-Status $upstream_cache_status;
        add_header X-FastCGI-Cache-Reason $skip_cache_reason;

        # Стандартные FastCGI-параметры
        include fastcgi_params;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

        # Таймауты
        fastcgi_read_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_connect_timeout 75;
    }

    # Статика — кэшируем надолго
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Запрещаем доступ к чувствительным файлам
    location ~ /\.ht {
        deny all;
    }

    location ~ /wp-config.php {
        deny all;
    }
}

Разберём ключевые моменты:

  • $skip_cache — переменная, которая определяет, нужно ли пропускать кэш. Если равна 1 — Nginx идёт в PHP
  • fastcgi_cache_valid 200 60m — успешные ответы кэшируются на 60 минут
  • fastcgi_cache_bypass — если $skip_cache = 1, кэш игнорируется (но всё равно обновляется)
  • fastcgi_no_cache — если $skip_cache = 1, ответ вообще не сохраняется в кэш
  • fastcgi_cache_use_stale — если PHP упал, отдаём устаревший кэш (лучше старая страница, чем 502)
  • fastcgi_cache_lock — если 100 пользователей одновременно запросили одну страницу, в PHP пойдёт только первый. Остальные подождут 5 секунд и получат готовый кэш
  • X-FastCGI-Cache-Status — заголовок для отладки. Показывает, откуда взята страница: HIT (из кэша), MISS (из PHP), BYPASS (пропущен кэш)

Шаг 4: Исключения для WooCommerce

WooCommerce — отдельная история. Нельзя кэшировать корзину, чекаут, личный кабинет. Иначе один пользователь увидит корзину другого. Это не просто ошибка — это катастрофа.

В конфиге выше уже есть все нужные исключения. Но давайте разберём их подробнее:

Что НЕ кэшируем:

  • /cart/ — корзина
  • /checkout/ — оформление заказа
  • /my-account/ — личный кабинет
  • /wc-api/ — устаревший эндпоинт WooCommerce, используется в основном для вебхуков/callback-ов платёжных систем
  • Страницы с куками woocommerce_cart_hash — если в корзине есть товары

Что кэшируем:

  • Главная страница
  • Каталог товаров
  • Карточки товаров
  • Статьи блога
  • Рубрики и метки

Это даёт идеальный баланс: 90% трафика обслуживается из кэша, а динамические страницы (корзина, чекаут) работают как обычно.

Шаг 5: Очистка кэша при публикации

Вот тут начинается магия. Когда вы публикуете новую статью или обновляете товар, кэш должен очиститься. Иначе пользователи будут видеть старую версию.

Есть два подхода: через PHP и через WP-CLI.

Подход А: Через PHP (добавляем в functions.php)

/**
 * Очистка FastCGI Cache при публикации/обновлении записи
 */
function customcode_purge_fastcgi_cache( $post_id = 0 ) {
    // Не выполняем при автосохранении
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }

    // Не выполняем для ревизий (защита на случай, если $post_id пустой —
    // например, при вызове из хука, который не передаёт ID записи)
    if ( $post_id && wp_is_post_revision( $post_id ) ) {
        return;
    }

    // Путь к папке кэша
    $cache_path = '/var/cache/nginx';

    // Очищаем весь кэш (проще и надёжнее)
    if ( is_dir( $cache_path ) ) {
        exec( 'rm -rf ' . escapeshellarg( $cache_path ) . '/*' );
    }
}
add_action( 'save_post', 'customcode_purge_fastcgi_cache' );
add_action( 'deleted_post', 'customcode_purge_fastcgi_cache' );
add_action( 'transition_post_status', function( $new_status, $old_status, $post ) {
    if ( $new_status === 'publish' || $old_status === 'publish' ) {
        customcode_purge_fastcgi_cache( $post->ID );
    }
}, 10, 3 );

// Очистка кэша при сохранении настроек темы через кастомайзер
add_action( 'customize_save_after', 'customcode_purge_fastcgi_cache' );

Две правки относительно «наивной» версии этого сниппета:

  • У функции теперь есть значение по умолчанию ($post_id = 0). Без него вызов из хука, который не передаёт ID записи (как customize_save_after), приведёт к фатальной ошибке ArgumentCountError на PHP 8.
  • В WordPress нет хука, который надёжно срабатывает «после сохранения настроек» в общем случае — options_general_page конкретно отвечает за рендер страницы «Настройки → Общие» в админке, а не за момент сохранения. Этот хук убран, а не оставлен как нерабочая заглушка.

Ещё одна оговорка: этот сниппет чистит весь кэш при каждом сохранении. Это просто и надёжно, но на сайте с частыми правками или несколькими авторами каждое сохранение вызывает короткую волну MISS-ов сразу после публикации (а в сочетании с fastcgi_cache_lock — ещё и ожидание до 5 секунд у некоторых запросов). Для большинства блогов это незаметно; для высоконагруженных сайтов с очень частыми правками стоит либо ограничить частоту purge (например, через transient-блокировку), либо перейти на избирательную очистку по конкретным URL.

Подход Б: Через Nginx Helper (плагин)

Если не хотите писать код, есть плагин Nginx Helper. Он весит 100 КБ, делает одну вещь — purge кэша. Устанавливаете, включаете, выбираете метод purge — обычно «Delete local server cache files» подходит для типичной одно-серверной установки. Всё.

Но учтите: плагин добавляет ещё одну зависимость. Если вы хотите полный контроль — пишите свой код (как выше).

Подход В: Через WP-CLI (для автоматизации)

# Очистка всего кэша
sudo rm -rf /var/cache/nginx/*

# Или через скрипт
sudo -u www-data find /var/cache/nginx -type f -delete

Можно добавить эту команду в скрипт деплоя. После git pull или rsync сразу чистим кэш.

Шаг 6: Мониторинг hit rate

Как понять, что кэш работает? Смотрим на заголовок X-FastCGI-Cache-Status.

Через curl:

curl -I https://custom-code.ru/

# Ожидаемый вывод:
# HTTP/2 200
# x-fastcgi-cache-status: HIT
# x-fastcgi-cache-reason:
# content-type: text/html; charset=UTF-8

Возможные значения:

  • HIT — страница взята из кэша. Идеально
  • MISS — страницы не было в кэше, Nginx сходил в PHP. При первом заходе — нормально. При повторном — проблема
  • BYPASS — кэш пропущен (админка, корзина, авторизованный пользователь). Нормально
  • EXPIRED — кэш устарел, Nginx обновит его. Нормально
  • STALE — PHP упал, отдаём устаревший кэш. Плохо, но лучше, чем 502

Скрипт для мониторинга:

Этот скрипт зависит от формата cache_log и строки access_log, добавленных в Шагах 2-3. Без них поле $upstream_cache_status просто не попадёт в лог, и скрипт всегда будет показывать 0%.

#!/bin/bash
# /usr/local/bin/fastcgi-cache-monitor.sh

# Считаем HIT/MISS за последний час
HITS=$(grep -c "upstream_cache_status: HIT" /var/log/nginx/access.log 2>/dev/null || echo 0)
MISSES=$(grep -c "upstream_cache_status: MISS" /var/log/nginx/access.log 2>/dev/null || echo 0)
BYPASS=$(grep -c "upstream_cache_status: BYPASS" /var/log/nginx/access.log 2>/dev/null || echo 0)

TOTAL=$((HITS + MISSES))

if [ "$TOTAL" -gt 0 ]; then
    HIT_RATE=$((HITS * 100 / TOTAL))
else
    HIT_RATE=0
fi

echo "=== FastCGI Cache Stats ==="
echo "HIT: $HITS"
echo "MISS: $MISSES"
echo "BYPASS: $BYPASS"
echo "Hit Rate: ${HIT_RATE}%"

# Алерт, если hit rate низкий
if [ "$HIT_RATE" -lt 80 ] && [ "$TOTAL" -gt 100 ]; then
    curl -s -X POST "https://api.telegram.org/bot$BOT_TOKEN/sendMessage" \
        -d chat_id="$CHAT_ID" \
        -d text="⚠️ FastCGI Cache: hit_rate=${HIT_RATE}% (HIT: $HITS, MISS: $MISSES)"
fi

Добавляем в cron:

# Каждый час
0 * * * * /usr/local/bin/fastcgi-cache-monitor.sh >> /var/log/fastcgi-cache-monitor.log 2>&1

Целевые показатели:

  • Hit rate > 95% — отличная настройка
  • Hit rate 80-95% — хорошо, но можно улучшить
  • Hit rate < 80% — проблема. Либо слишком много исключений, либо кэш быстро истекает

Шаг 7: Нюансы для WooCommerce

Для магазинов соблазнительно дополнительно кэшировать AJAX/API-запросы WooCommerce. Здесь нужно быть аккуратным — большую часть этого трафика на самом деле кэшировать нельзя:

  • AJAX-запрос «добавить в корзину», который используют темы WooCommerce, идёт через admin-ajax.php (то есть /wp-admin/admin-ajax.php?action=...) — а этот путь уже исключён правилом /wp-admin/ из Шага 3. Он обязан выполняться каждый раз, потому что читает и пишет сессию/корзину — закэшировав его, вы покажете пользователям чужую или устаревшую корзину.
  • /wc-api/ — это устаревший эндпоинт WooCommerce, используется в основном для вебхуков/callback-ов платёжных систем (IPN и похожее). Это обычно POST-запросы с данными конкретного заказа, так что кэшировать там почти нечего, а POST-запросы и так уже исключены.

На практике кэширование на уровне страниц из Шага 3 (каталог, карточки товаров, рубрики) уже покрывает подавляющую часть трафика WooCommerce, который безопасно кэшировать. Если хочется пойти дальше — можно посмотреть на кэширование read-only, неперсонализированных ответов REST API под /wp-json/wc/store/ с короткими TTL (секунды, не минуты) — но тестируйте внимательно, эти эндпоинты часто зависят от сессии/состояния корзины.

Шаг 8: Решение частых проблем

Кэш не работает (всегда MISS)

Проверьте три вещи:

  1. Права на папку кэша:
    sudo chown -R www-data:www-data /var/cache/nginx
    sudo chmod -R 755 /var/cache/nginx
  2. Конфиг Nginx:
    sudo nginx -t
    sudo systemctl reload nginx
  3. Куки WordPress: если вы авторизованы — кэш пропускается. Откройте сайт в режиме инкогнито и проверьте снова

Админка кэшируется (пользователи видят админ-панель)

Значит, не работает правило if ($request_uri ~* "/wp-admin/"). Проверьте, что оно стоит до блока location /.

После публикации статья не обновляется

Значит, не работает purge. Проверьте:

  • Код в functions.php добавлен
  • Права у пользователя www-data на папку /var/cache/nginx
  • Команда rm -rf /var/cache/nginx/* выполняется вручную
  • exec() не отключён в php.ini (некоторые managed-хостинги отключают её — проверьте директиву disable_functions)

WooCommerce показывает чужую корзину

Это критическая ошибка! Значит, не работают исключения для WooCommerce. Проверьте правила:

if ($request_uri ~* "/cart/|/checkout/|/my-account/") {
    set $skip_cache 1;
}

if ($http_cookie ~* "woocommerce_cart_hash|woocommerce_items_in_cart") {
    set $skip_cache 1;
}

И убедитесь, что они стоят до блока location ~ \.php$.

Кэш занимает слишком много места

Уменьшите время хранения:

fastcgi_cache_valid 200 30m;  # Было 60m
inactive=30m;  # Было 60m

Или настройте автоматическую очистку через cron:

# Каждый день в 3 часа ночи
0 3 * * * find /var/cache/nginx -type f -mtime +1 -delete

Чек-лист настройки FastCGI Cache

  • ✅ Проверена поддержка http_fastcgi_module в Nginx
  • ✅ Создана зона кэша в nginx.conf (fastcgi_cache_path)
  • ✅ Добавлен формат лога cache_log и подключён через access_log
  • ✅ Настроены правила кэширования в конфиге сайта
  • ✅ Добавлены исключения для админки, авторизованных пользователей, WooCommerce
  • ✅ Настроена очистка кэша при публикации (через PHP или WP-CLI)
  • ✅ Добавлен заголовок X-FastCGI-Cache-Status для отладки
  • ✅ Проверена работа через curl -I (статус HIT)
  • ✅ Настроен мониторинг hit rate с алертами
  • ✅ Hit rate стабильно выше 90%
  • ✅ TTFB упал до 10-20 мс для закэшированных страниц

Реальные цифры (до и после)

Вот характерный пример с одного проекта — 50 000 страниц, 10 000 посетителей в день. Ваши цифры будут отличаться в зависимости от трафика, хостинга и того, насколько кэш-дружелюбны тема и плагины:

Метрика До FastCGI Cache После FastCGI Cache Улучшение
TTFB (главная) 850 мс 12 мс 70×
TTFB (статья) 720 мс 15 мс 48×
TTFB (каталог) 1.2 сек 18 мс 66×
CPU (средняя нагрузка) 45% 12% 3.7×
PHP-FPM workers (пиковая) 50/50 8/50 6.2×
Запросов к MySQL в минуту 12 000 800 15×
FastCGI Cache hit rate 96.4%

Разница колоссальная. Сервер стал есть в 4 раза меньше CPU, MySQL получил в 15 раз меньше запросов, а пользователи видят страницы мгновенно.

Итог

FastCGI Cache — это не просто «ещё один способ кэширования». Это фундаментальное изменение архитектуры. Вы перестаёте гонять каждый запрос через PHP и MySQL. Nginx становится умным прокси, который отдаёт готовые страницы за миллисекунды.

Настройка занимает 30 минут. Но эффект вы почувствуете сразу. TTFB упадёт в 50-70 раз. CPU разгрузится. База данных выдохнет. А PageSpeed Insights наконец-то покажет зелёные цифры.

И самое главное: это бесплатно. Не нужно покупать WP Rocket за $50 в год. Не нужно ставить тяжёлые плагины. Всё уже встроено в Nginx. Нужно только настроить.

Откройте терминал прямо сейчас. Добавьте зону кэша в nginx.conf. Настройте правила в конфиге сайта. Перезагрузите Nginx. И посмотрите, как ваш сайт превращается из тихохода в ракету.

Вы удивитесь, как много ресурсов пожирал PHP. И как легко было это исправить.