Оптимизация статики: sendfile, gzip, кеш и expires

Статика и так быстрая, но горстка директив делает её молниеносной: меньше CPU, меньше трафика, меньше повторных запросов.
«Сожми текст, отдавай через ядро, скажи браузеру кешировать на год — и твой сервер выдохнет.»

Базовая отдача файлов работает из коробки. Но чтобы выжать максимум, нужно настроить три вещи: эффективную отправку байтов, сжатие и кеширование. Разберём по порядку.

sendfile и TCP-оптимизации

http {
    sendfile on;        # отдача файла напрямую ядром (zero-copy)
    tcp_nopush on;      # склеить заголовки и начало файла в один пакет
    tcp_nodelay on;     # не задерживать мелкие пакеты (отключить Nagle)
}

sendfile переносит данные с диска в сеть силами ядра, минуя копирование в пользовательскую память — заметно экономит CPU. tcp_nopush работает в паре с ним: отправляет HTTP-заголовки и первый кусок файла одним пакетом. tcp_nodelay важен для множества мелких ответов.

Сжатие

gzip on;
gzip_comp_level 5;
gzip_min_length 256;            # не жать крошечные файлы
gzip_types text/plain text/css application/json
           application/javascript text/xml application/xml;
gzip_vary on;                   # корректный кеш на прокси/CDN

Сжатие уменьшает текстовые ответы в разы. gzip_min_length 256 отключает сжатие для совсем мелких файлов (накладные расходы gzip сделают их больше). Картинки (JPEG/PNG) и так сжаты — их не трогаем. Brotli даёт на 15–25% лучше, если установлен модуль.

Кеширование браузером

location ~* \.(css|js|jpg|jpeg|png|gif|svg|woff2)$ {
    expires 1y;                          # Expires + Cache-Control: max-age
    add_header Cache-Control "public, immutable";
    access_log off;                      # не логировать каждую иконку
}

expires 1y; ставит и Expires, и Cache-Control: max-age. immutable говорит браузеру «этот файл не изменится, даже не перепроверяй» — идеально для версионированных ассетов вроде app.7f3a.js.

Кеш файловых дескрипторов

open_file_cache max=10000 inactive=30s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;

Как работает под капотом

open_file_cache запоминает открытые дескрипторы файлов, их размеры и время изменения. При повторной отдаче того же файла Nginx не делает заново open() и stat() — операции ввода-вывода резко сокращаются. Для популярной статики это ощутимый выигрыш. Сжатие gzip выполняется на лету в воркере, но результат можно и закешировать (gzip_static с заранее сжатыми .gz-файлами).

Частые ошибки

  • Жать уже сжатое. Включать gzip для JPEG/PNG/видео — пустая трата CPU.
  • immutable без версионирования имён. Если файл style.css не меняет имя при обновлении, immutable заставит браузеры держать старую версию. Версионируй имена (хеш в названии).
  • Логировать каждую иконку. Раздутый access.log из-за статики; access_log off для ассетов спасает.

Best practices

  • Версионируй имена статики (хеш) + expires 1y; immutable — пользователь скачает файл один раз.
  • Сжимай только текстовые типы; не трогай уже сжатые форматы.
  • Включи open_file_cache на серверах с тяжёлой статикой.

Стратегия кеширования: что и насколько

Грамотное кеширование требует разной политики для разного контента — единый expires на всё только навредит. Версионированные ассеты (файлы с хешем в имени, вроде main.a1b2c3.js) можно кешировать максимально агрессивно: expires 1y; immutable, потому что при любом изменении меняется имя, и старая версия никогда не «застрянет». HTML-страницы, наоборот, кешировать надолго опасно: пользователь должен сразу видеть свежий контент, поэтому им ставят короткий срок или no-cache. Картинки и шрифты — где-то посередине, обычно недели или месяцы.

Стоит понимать и разницу между кешем браузера и кешем самого Nginx. Всё, о чём шла речь (expires, Cache-Control), управляет кешем в браузере пользователя — это убирает повторные запросы вообще. Но Nginx умеет и кешировать ответы бэкенда у себя через proxy_cache: тогда популярную динамическую страницу он отдаёт из своего кеша, не дёргая приложение каждый раз. Это мощный приём для контентных сайтов, но требует аккуратности с инвалидацией — нельзя показать пользователю чужие данные из кеша. Начинать стоит с простого: версионируй статику, кешируй её на год, сжимай текстовые типы — этого уже достаточно, чтобы ощутимо разгрузить сервер и ускорить загрузку страниц для повторных посетителей.

Итоги

Скорость статики складывается из sendfile + TCP-настроек (меньше CPU и пакетов), сжатия текстовых типов (меньше трафика), агрессивного кеширования браузером через expires/immutable (меньше повторных запросов) и open_file_cache (меньше дискового I/O). Раздачу статики освоили — переходим к главной суперсиле Nginx: обратному прокси.

Проверьте себя
1. Зачем нужна директива sendfile on?
AСжимает ответы
BПозволяет ядру отдавать файл напрямую в сокет без копирования в память приложения (zero-copy), экономя CPU
CШифрует трафик
DЛогирует запросы
2. Когда безопасно ставить `Cache-Control: immutable` на ассет?
AВсегда
BКогда имя файла версионируется (например, содержит хеш), так что при изменении меняется и имя
CТолько для HTML
DТолько при HTTP без шифрования