Серверные включения (SSI) в Nginx — недооценённый инструмент, который позволяет собирать итоговую HTML‑страницу из независимых блоков и запрашивать эти блоки параллельно как подзапросы. В связке с прокси‑кэшем это превращается в удобный «конструктор» производительности: тяжёлые, но редко меняющиеся части страницы можно кэшировать на уровне Nginx, а динамику оставлять приложению. Ниже — практическая методика, безопасные шаблоны и тонкости конфигурации, чтобы выжать максимум производительности без усложнения кода приложения.
Что такое SSI и как они работают в Nginx
SSI (Server Side Includes) — это простой язык директив, встроенный в ответ HTML, который Nginx обрабатывает фильтром перед отдачей клиенту. Ключевая возможность — директива include, которая инициирует подзапрос к URI на этом же Nginx и подставляет его вывод в место вызова. Для вас это означает: можно хранить страницу‑шаблон как «каркас», а меню, шапку, подвал, панель пользователя, виджеты — получать отдельными subrequest и кэшировать их по своим правилам.
Важно понимать, что подзапрос в контексте Nginx — это не отдельное HTTP‑соединение от браузера. Это внутренний запрос (internal) внутри воркера Nginx, который может проксироваться к апстриму (PHP‑FPM через FastCGI, приложению за proxy_pass и т.д.) или извлекаться из кэша. Подзапросы лёгкие, полностью неблокирующие и выполняются параллельно.
Где SSI уместны
SSI отлично подходят, когда:
- страница состоит из независимых блоков с разной динамикой (шапка, навигация, авторизационный блок, рекомендации, подвал);
- вы хотите кэшировать статичные и «псевдодинамические» фрагменты без кэширования всей страницы;
- нужно уменьшить TTFB для тяжёлых участков, отдавая их из кэша Nginx;
- приложение исторически монолитно, но есть потребность вынести часть нагрузки на уровень веб‑сервера;
- надо распараллелить получение блоков (subrequest запускаются одновременно).
SSI — это не замена шаблонизатора в приложении. Это способ догнать производительность там, где переписать код быстро невозможно, и при этом чётко контролировать кэш на уровне фронтенд‑сервера.

Включаем SSI: базовая конфигурация
SSI включаются в location, который отдаёт итоговую HTML‑страницу. Минимальный набор:
server {
listen 80;
server_name example.com;
# Итоговая страница собирается с SSI
location / {
ssi on;
ssi_types text/html; # по умолчанию text/html, можно расширить
# Если бэкенд сжимает ответы, а вы хотите фильтровать их SSI
gunzip on; # модуль gunzip должен быть доступен
proxy_pass http://app;
proxy_set_header Host $host;
}
}
Модуль SSI не умеет разбирать уже сжатый контент. Вариантов два: отключить gzip на бэкенде для страниц с SSI или включить в Nginx директиву gunzip on, чтобы сервер мог распаковать проксируемый ответ, обработать SSI и снова сжать (если надо). Это критическая мелочь: без неё SSI просто не сработает.
Синтаксис SSI: включаем блоки
В самом HTML‑каркасе страницы вставляете SSI‑директивы. Для подзапроса используйте include virtual — он создаёт внутренний HTTP‑запрос к указанному URI (будет применён весь конфиг локации) и встроит ответ:
<!-- каркас HTML -->
<div class="layout">
<header>
<!--# include virtual="/_frag/header" -->
</header>
<nav>
<!--# include virtual="/_frag/menu" -->
</nav>
<main>
<!-- контент приложения -->
</main>
<aside>
<!--# include virtual="/_frag/reco" -->
</aside>
<footer>
<!--# include virtual="/_frag/footer" -->
</footer>
</div>
Для чтения локальных файлов существует include file, но на проде предпочтительнее virtual, чтобы обрабатывать блоки через Nginx‑кэш и апстримы.
Дизайн подзапросов: internal‑локейшены для фрагментов
Хорошая практика — обрабатывать фрагменты в специальном префиксе, который недоступен извне. Тогда эти URI можно вызывать только как подзапросы из SSI, а не напрямую из браузера. Простой шаблон:
http {
proxy_cache_path /var/cache/nginx/frag levels=1:2 keys_zone=frag_cache:50m max_size=1g inactive=30m use_temp_path=off;
# Сегментация кэша гостей/авторизованных
map $cookie_session $user_segment {
default guest;
~.+ auth;
}
# Условие для обхода кэша при наличии приватных куки
map $http_cookie $bypass_cache {
default 0;
~*session= 1;
}
server {
listen 80;
server_name example.com;
location / {
ssi on;
ssi_types text/html;
gunzip on;
proxy_pass http://app;
proxy_set_header Host $host;
}
# Фрагменты доступны только как подзапросы
location /_frag/ {
internal;
proxy_pass http://app;
proxy_cache frag_cache;
proxy_cache_valid 200 5m;
proxy_cache_lock on;
proxy_cache_lock_timeout 10s;
proxy_cache_background_update on;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
# Ключ кэша с сегментацией по типу пользователя
proxy_cache_key "$scheme|$proxy_host|$request_uri|$user_segment";
# Обход кэша для приватных ответов
proxy_no_cache $bypass_cache;
proxy_cache_bypass $bypass_cache;
proxy_set_header Host $host;
}
}
}
Здесь есть несколько важных моментов:
internalзакрывает прямой доступ к фрагментам; браузер получит 403, что корректно. Только SSI внутри Nginx может запрашивать эти URI.- Ключ кэша добавляет
$user_segment, чтобы разнести блоки для гостей и авторизованных. Это грубая, но простая сегментация (две корзины вместо миллионов вариаций). Если блок вообще не должен кэшироваться для авторизованных, используйтеproxy_no_cacheиproxy_cache_bypassпо наличию приватных куки. proxy_cache_lockиproxy_cache_background_updateустраняют «шторм» кэша и не блокируют клиентские ответы при истечении TTL.
Кэширование подзапросов: TTL, валидация, заголовки
Стратегию TTL стоит выбирать отдельно для каждого блока. Например, меню можно кэшировать 30 минут, рекомендации — 1 минуту, подвал — сутки. Для этого удобно использовать разные локации или location ~ ^/_frag/(reco|menu|footer)$ с разными proxy_cache_valid.
Если бэкенд проставляет корректные Cache-Control, включите проверку валидности через proxy_cache_revalidate on, чтобы Nginx при истечении TTL пытался сделать условный запрос (If-Modified-Since/ETag) и экономил трафик и CPU. Это особенно полезно для больших фрагментов, которые меняются редко.
Помните, что для внутренних фрагментов не нужен Vary — их никто не запрашивает снаружи. Но если блок может быть запрошен напрямую (например, выделили его в публичный ресурс), корректно укажите Vary: Cookie или иные факторы вариативности, либо запретите прямой доступ через internal.

Производительность и масштабирование
Подзапросы выполняются параллельно, что снижает латентность совокупной страницы. Но с ростом числа SSI‑включений вы увеличиваете фан‑аут к бэкенду. Несколько практических правил:
- Держите число SSI‑блоков разумным. 3–8 фрагментов обычно безболезненны. Десятки — мониторьте бэкенд и коннекты.
- Обязательно включайте keepalive к апстриму (proxy_pass) и увеличьте
keepaliveв апстрим‑блоке, чтобы не плодить соединения. - Давайте более длинные TTL для тяжёлых и стабильных блоков, короткие — для легковесных и динамичных.
- Используйте
proxy_cache_lock, чтобы один клиент «прогревал» фрагмент, а остальные получали старую копию (use_stale updating). - Включите логирование статуса кэша в access‑лог, чтобы видеть попадания/промахи по фрагментам.
log_format main '$remote_addr - $request $status $body_bytes_sent '
'cache:$upstream_cache_status rt:$request_time urt:$upstream_response_time';
access_log /var/log/nginx/access.log main;
Смежные материалы: посмотрите про оптимизацию изображений WebP/AVIF через map в Nginx в статье «Оптимизация изображений WebP/AVIF через map и кэш» (WebP/AVIF через map) и про уровни кеша приложения в «Memcached и Redis для PHP» (Memcached/Redis для PHP).
Типовые шаблоны сборки
1) Каркас с кэшируемыми «псевдостатическими» блоками
Шапка, меню, подвал хранятся в кэше минутами/часами. Основной контент приходит от приложения без кэша. Это резко снижает нагрузку на бэкенд и ускоряет TTFB на пике.
2) Гостевой/авторизованный сегменты
Гостям показываем кэш, авторизованным — кэш ограниченно или вовсе его обход. Для этого достаточно двух корзин кэша через $user_segment. Если авторизованных много и блок персонализирован — лучше не кэшировать и отдавать напрямую, иначе получится «взрыв» вариативности кэша.
3) «Горячие» блоки с микрокэшем
Некоторые динамические блоки можно кэшировать буквально 1–5 секунд, сглаживая пики. Подобная микрокэш‑стратегия на фрагментах отлично переживает всплески трафика и защищает базу данных от шторма.
Безопасность: запреты и гигиена заголовков
Несколько правил безопасности при использовании SSI:
- Все фрагменты — только за
internal. Это защищает от обхода и перечисления. - Не прокидывайте лишние заголовки клиента в подзапросы. Строго ограничьте список требуемых
proxy_set_header. Особенно аккуратнее с Cookie и Authorization. - Не используйте пользовательский ввод напрямую в SSI‑директивах. Если передаёте параметры из URI в подзапросы, валидируйте их на стороне приложения или через
map/ifс whitelisting. - Контролируйте размер значений SSI через
ssi_value_length, если используете длинные переменные в директивах SSI.
Совместимость с сжатием и фильтрами
SSI — это фильтр, который должен видеть исходный текст HTML. Если бэкенд прислал gzip, модуль SSI не сможет распарсить ответ. Варианты:
- Отключить gzip на бэкенде для HTML с SSI. Сжимать уже на Nginx после обработки SSI.
- Включить
gunzip onв Nginx, чтобы сервер прозрачным образом распаковал ответ, применил SSI и снова сжал.
Также проверьте список типов, для которых включён SSI: по умолчанию text/html. Если шаблон отдаётся как application/xhtml+xml или text/xml, добавьте эти типы в ssi_types.
Отладка и диагностика
Несколько трюков, чтобы убедиться, что всё работает как задумано:
- Включите логирование
$upstream_cache_statusи смотрите, что происходит по URI/_frag/.... «HIT» — хорошо, «MISS» — прогрев, «BYPASS/EXPIRED/UPDATING» — тоже ожидаемо. - Временное логирование заголовков ответа фрагмента через
add_headerпомогает понять, не конфликтуют ли заголовки кэша, нет ли Set-Cookie, который ломает шаринг. - Проверяйте, не делает ли приложение редиректы на фрагменты (это ломает подзапросы). Для фрагментов отдавайте
200с нужной разметкой без редиректов.
Расширенные настройки кэш‑слоя для фрагментов
Если используете лимиты запросов, определите зону в контексте http:
limit_req_zone $binary_remote_addr zone=fragburst:10m rate=10r/s;
Если блоки тяжёлые и популярные, используйте дополнительные предохраняющие настройки:
location /_frag/reco {
internal;
proxy_pass http://app;
proxy_cache frag_cache;
proxy_cache_key "$scheme|$proxy_host|$request_uri|$user_segment";
proxy_cache_valid 200 1m;
proxy_cache_lock on;
proxy_cache_lock_age 20s;
proxy_cache_lock_timeout 10s;
proxy_cache_background_update on;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
# Защита от перегрева бэкенда
limit_req zone=fragburst burst=20 nodelay;
}
Лимиты запросов к фрагментам особенно полезны при инцидентах: страница может запускать несколько подзапросов, и даже небольшой всплеск трафика превращается в лавину обращений к бэкенду. Мягкий троттлинг и ста́лый кэш позволяют пережить пики.
Ошибки и крайние случаи
- «SSI не работают» — проверьте, что ответ не сжат до SSI, и что в локации итоговой страницы реально включено
ssi on. Убедитесь, что контент‑тайп входит вssi_types. - «Падает производительность» — вы, вероятно, сделали слишком много фрагментов или слишком короткий TTL. Начните с 3–5 блоков и TTL в 1–5 минут для «тяжёлых», остальное без кэша.
- «В кэше оказываются приватные данные» — проверьте
proxy_no_cache/proxy_cache_bypassпо кукам и заголовкам. Лучше вообще не кэшировать персонализированный блок, если вариативность высока. - «Фрагмент редиректит» — не делайте редиректов в подзапросах; отдавайте прямо фрагмент с
200 OK. Редирект в subrequest не подставится как HTML.
Ограничения и ожидания
SSI — это простой, но мощный инструмент. У него нет логики уровня полноценного шаблонизатора, а вложенные SSI и сложные условия лучше не использовать. Держите правила простыми: include нужных блоков, минимум логики в SSI, максимум — в приложении и конфиге Nginx. В обмен вы получаете параллельные подзапросы, гибкое фрагментное кэширование и предсказуемое поведение на уровне фронтенда.
Практический чеклист внедрения
- Выделите 3–6 блоков страницы, которые логично кэшировать отдельно (header/menu/footer/reco/user‑panel).
- Соберите каркас страницы и вставьте SSI
include virtualдля каждого блока. - Создайте
/_frag/локации сinternalи настройтеproxy_cacheс разумными TTL и защитой от шторма. - Сегментируйте гостей/авторизованных через map и добавьте сегмент в ключ кэша или полностью отключите кэш для персональных блоков.
- Включите
gunzip onи проверьтеssi_types. - В лог добавьте
$upstream_cache_statusи проверьте долю HIT после прогрева.
Итоги
Связка «nginx + ssi + subrequest + кэш» — быстрый способ ускорить выдачу, разгрузить приложение и базу, а также повысить устойчивость на пиках. Она особенно органична на VDS: вы контролируете конфигурацию, дисковое пространство под кэш и сетевые лимиты. Начните с минимального набора фрагментов, настройте кэш‑слой и наблюдайте за метриками — вы увидите, как снижается время ответа и нагрузка на бэкенд. Главное — держать архитектуру простой: небольшое число блоков, предсказуемые TTL, аккуратная сегментация и жёсткое правило «internal для всего, что связано с SSI».


