Знаете, что меня всегда удивляет? Люди ставят 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)
Проверьте три вещи:
- Права на папку кэша:
sudo chown -R www-data:www-data /var/cache/nginx sudo chmod -R 755 /var/cache/nginx - Конфиг Nginx:
sudo nginx -t sudo systemctl reload nginx - Куки 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. И как легко было это исправить.