Один из самых простых способов ускорить работу сайтов и снизить нагрузку на инфраструктуру — корректно использовать клиентское кэширование. Механизм одновременно и простой, и сложный. В этой статье посмотрим, как можно управлять клиентским кэшированием в веб‑сервере Angie.
Навигация по циклу
Настройка location в Angie. Разделение динамических и статических запросов.
Перенаправления в Angie: return, rewrite и примеры их применения.
Сжатие текста в Angie: статика, динамика, производительность.
Клиентское кэширование в Angie.
Видеоверсия
Для вашего удобства подготовлена видеоверсия этой статьи, доступна на Rutube, VKVideo и YouTube.
Механизм кэширования
При первом посещении типичного современного сайта браузеру необходимо сначала установить соединение с сервером, обычно по HTTPS, чтобы запросить и получить HTML‑документ. Далее он должен распарсить этот документ и запланировать десятки запросов на дополнительные объекты: картинки, листы стилей, файлы с кодом JS, подключаемые шрифты и так далее В результате при загрузке одной типовой страницы происходит около 100 запросов и передается несколько мегабайт.
Следом пользователь переходит на другую страницу сайта, где находятся еще около 100 объектов, то есть процесс должен повториться. При более детальном анализе окажется, что более половины объектов уже были загружены браузером и при переходе можно их повторно не загружать. Именно эту функцию и реализует клиентское кэширование.
Как правило, наиболее агрессивно кэшируют статику — запросы к файлам на диске сервера. Сюда относятся картинки, шрифты, CSS, JS и другие подобные элементы. Они редко меняются и часто составляют основную долю трафика страниц. Динамические же документы (HTML, JSON и так далее) обычно не кэшируют, а могут даже явно запрещать сохранять в кэше. Но возможны исключения, когда динамический документ аккуратно кэшируется на короткое время.
Для работы кэша нужно две стороны: клиент и сервер. Задача сервера — при ответе пользователю установить заголовки, разрешающие или запрещающие кэширование. Однако, нужно помнить, что даже при отсутствии заголовков кэширования клиент может кэшировать контент для сокращения количества запросов исходя из эвристических правил. Для явного отключения кэша рекомендуется использовать заголовок Cache-Control.
Рассмотрим следующий пример:
Cache-Сontrol: public, max-age=3600
Etag: "04bd84392596ea1979453727b666caa9"
Expires: Fri, 22 Aug 2025 13:46:37 GMT
Last-Modified: Tue, 19 Aug 2025 17:49:13 GMT
Здесь использованы сразу все варианты заголовков кэширования. Получив такие заголовки, клиент может использовать кэш для хранения объекта. Также некоторые из них могут помогать эффективной работе поисковых роботов, чтобы сократить ненужные запросы на сервер. Принцип действия каждого из этих заголовков мы разберём ниже.
Кэширование может происходить не только на клиенте, но и на промежуточных серверах (прокси, узлы CDN и так далее) В этом случае говорят о разделяемом или общем (shared) кэше. Механизм действия тот же, но иногда мы можем управлять сохранением конкретного ресурса в клиентском (private) или общем (public) хранилище, например с помощью заголовка Cache‑Control.
Заголовок Etag
Первый заголовок в нашем разборе – это Etag (entity tag). Он, по сути, является кратким дайджестом контента запроса или версией документа. Конкретный алгоритм формирования Etag не определён в спецификации — это может быть хэш контента, хэш даты обновления или версии. Пример заголовка в Angie показан ниже.
Etag "688a29f2-1597"
По умолчанию формирование заголовка Etag в Angie включено для статических запросов. Основные входные данные для Etag — это время модификации файла и его размер. Управление директивой etag доступно на уровне http, server и location. Отключить генерацию в любом из этих блоков можно следующей строчкой:
etag off;
Получив ответ с заголовком Etag, клиент сохранит его в кэш и при следующих запросах будет проводить валидацию с помощью заголовка If-None-Match.
If-None-Match: "688a29f2-1597"
Если документ на сервере имеет такой же Etag (то есть не был изменён), то клиент получит ответ без тела (только заголовки) с кодом 304 (Not Modified).
У заголовка Etag существует разновидность со слабым валидатором (weak validator). В этом случае перед значением добавляется префикс «W/». При использовании слабой валидации ответы с одинаковым значение тэга могут быть эквивалентны семантически, но отличаться по содержанию. Например, у веб‑страницы могут быть различные рекламные блоки или значение даты в тексте. Слабые Etag позволяют кэшировать документ целиком, но не дают кэшировать запросы диапазонов (range requests). Angie применяет weak etag при компрессии и декомпрессии, ресайзе картинки в image‑фильтре, addition‑фильтре, xslt‑преобразовании с опцией xslt_last_modified on, ssi c директивой ssi_last_modified on, sub‑фильтре и sub_filter_last_modified on. Пример слабого Etag показан ниже.
Etag: W/"68c2f468-30b6"
Использование заголовка Etag можно рекомендовать для динамических запросов, где требуется постоянная валидация и свежие данные. В этом случае заголовок формирует бэкенд на основе хэша даты модификации контента или самого контента.
Заголовок Last-Modified
Как мы говорили ранее, заголовок Etag в Angie использует время модификации документа в качестве одного из параметров. При этом есть заголовок с чистым временем изменения документа: Last-Modified. Его действие похоже на Etag, но более прозрачно по механизму формирования и семантике. Например, при отдаче статических файлов заголовок будет иметь значение mtime (в GMT-формате) из файловой системы:
Last-Modified: Fri, 06 Sep 2024 16:10:23 GMT
При получении документа с таким заголовком браузер может сохранить его в кэше (если другие заголовки это не запрещают). Далее при повторном запросе происходит валидация с помощью заголовка If-Modified-Since.
If-Modified-Since: Fri, 06 Sep 2024 16:10:23 GMT
Как и в случае с Etag, сервер должен проверить ответ и при отсутствии более свежей версии вернуть только заголовки со статусом 304 (Not Modified). Если ресурс обновился, будет обычный ответ с заголовками и телом, код 200.
В Angie заголовок Last‑Modified отдельно не регулируется, а автоматически выставляется для статических запросов (отдача файлов с диска). Если запрос проксируется, то заголовок по умолчанию передаётся клиенту. При раздаче одних и тех же файлов с различных серверов стоит позаботиться о синхронизации атрибута mtime для этих файлов, чтобы кэширующие заголовки не зависели от сервера, выбранного для ответа.
Как и Etag, заголовок Last‑Modified подходит для динамических запросов, может устанавливаться бэкендом на основе даты редактирования документа. Также он полезен для поисковых роботов, позволяет избежать лишних переиндексаций страниц сайтов.
Заголовок Expires
Предыдущие заголовки предполагают механизм валидации при повторном использовании кэшированных ресурсов. Это влечет за собой создание запросов и получение ответов с кодом 304, что может быть излишним, если мы говорим о полностью статических документах. Здесь выходит на сцену заголовок Expires (и Cache‑Control, но о нём чуть позже). Пример такого заголовка показан ниже.
Expires: Tue, 25 Aug 2026 14:34:46 GMT
С помощью заголовка Expires мы задаём TTL (Time To Live, время жизни) для элементов кэша. При этом до истечения этого срока клиент не должен проверять обновления документа. Так мы избавляемся от лишних условных запросов, но теряем контроль за обновлением документа со стороны сервера. Установить заголовок Expires (и одновременно Cache-Control) можно с помощью директивы expires.
expires 1y;
Как правило, для статических документов указывается большое время жизни кэша. В нашем примере это один год от текущей даты и времени (1y), но есть и специальное значение max (2037 год). Директива также поддерживает параметр modified, который меняет алгоритм расчета значения даты на сумму даты модификации файла и параметра времени. Отрицательные значения будут указывать на то, что ресурс уже устарел (отключает кэширование).
Таким образом, заголовок оптимален для работы с «вечным» кэшем статических ресурсов. Однако, инвалидация кэша теперь должна происходить на уровне веб‑приложения. Что может быть лучше для статики? Только заголовок Cache‑Control.
Заголовок Cache-Control
Вот мы и добрались до самого могучего заголовка клиентского кэширования. Заголовок Cache‑Control работает по аналогии с Expires (не требует условных запросов), но имеет более гибкие настройки. Если установлен заголовок Cache‑Control, значение Expires игнорируется клиентом. Давайте рассмотрим пример. С помощью этого заголовка можно реализовать «вечный кэш» статики с множеством уточняющих его поведение параметров.
Cache-Control: max-age=31536000, public, no-transform, immutable
Значение времени жизни кэша здесь указывается относительно текущего времени в секундах (max-age). Это гораздо удобнее использовать в конфигурации, так как не требуется проводить никаких расчётов времени. Параметр max-age может равняться нулю, что означает запрет кэширования.
Помимо срока кэширования в заголовке можно указать множество тонких настроек кэширования. В примере выше мы разрешаем кэшировать элемент в публичном хранилище (public), запрещаем изменять и оптимизировать (no‑transform) его промежуточным сервисам (например, сервисам экономии трафика или оптимизатором картинок). Наконец, мы говорим, что ресурс не нужно валидировать (immutable), пока он находится в пределах срока жизни кэша.
Также есть возможность разрешить использовать устаревший элемент кэша, пока происходит валидация с параметром stale‑while‑revalidate. Пример показан ниже.
Cache-Control: max-age=604800, stale-while-revalidate=86400
Здесь мы устанавливаем время жизни кэша на 7 дней, но в течение суток после этого можно использовать устаревший элемент, пока происходит валидация.
Запрет на кэширование устанавливается с помощью параметров no-store и no-cache. Первый из них полностью запрещает кэширование в любом виде, а второй разрешает кэш, но с обязательной валидацией ответа при каждом использовании. Поэтому полное отключение кэша выглядит так.
Cache-Control: no-store
Многие параметры заголовка могут использоваться в запросах. Например, при жестком обновлении страницы может отправляться заголовок запроса Cache‑Control.
Cache-Control: no-cache
Конфигурация заголовка Cache-Control в Angie довольно проста и выполняется с помощью универсальной директивы add_header. Логично включать его точечно в локациях со статикой:
location /static/ {
add_header Cache-Control "max-age=31536000, public, no-transform, immutable";
}
Проверить наличие заголовков кэширования можно в средствах разработчика браузера (DevTools) или с помощью curl:
curl --head http://localhost/images/bg.jpg
HTTP/1.1 200 OK
Server: Angie/1.10.1
Date: Tue, 26 Aug 2025 09:29:45 GMT
Content-Type: image/jpeg
Content-Length: 37864
Last-Modified: Fri, 06 Sep 2024 15:38:16 GMT
Connection: keep-alive
Vary: Accept-Encoding
ETag: "66db21e8-93e8"
Cache-Control: max-age=31536000, public, no-transform, immutable
Accept-Ranges: bytes
Обратите внимание на заголовок ответа Vary. Он также помогает решить задачу корректного кэширования ресурсов. Его значение показывает заголовки ответа, от которых может зависеть ответ. Например, ответ может быть сжат различными кодеками, поддержку которых сервер определяет по заголовку Accept-Encoding. Это важно, если ответ будет кэширован в общем хранилище.
Сброс “вечного кэша”
Допустим, мы установили максимально разрешающий заголовок для кэширования ресурсов в локации со статическими файлами. Но что делать, если ресурс обновится? Он уже закэширован у клиента, и мы разрешили не валидировать его при использовании. Это значит, что запросов к серверу не будет. Не просить же всех посетителей нашего сайта вызывать принудительное обновление сайта через нажатие Ctrl+F5 или средства разработчика?
Решение для обновления элементов в вечном кэше простое: необходимо менять адрес ресурса. Сделать это можно в части пути к ресурсу (потребует использование перенаправлений или переименования файлов) или с помощью дополнительных GET‑параметров (не требует дополнительных действий). Что именно добавлять в путь или параметры запроса, зависит от фантазии разработчиков приложения: хэш контента, версия, дата модификации и так далее Главное, чтобы каждый раз при изменении файла на диске адрес ресурса менялся. При этом сам документ, где содержатся ссылки на эти ресурсы (например: CSS, JS, картинки), не должен кэшироваться на длительное время без валидации. Примеры схем сброса кэша показаны ниже.
/static/oaf320e34d/1.css
/static/1.oaf320e34d.css
/static/1.css?ver=2
/static/1.css?20250820
/static/1.css?ts=1756197781
В самом документе ресурсы указываются с учетом версии:
<link href="/static/1.oaf320e34d.css" rel=stylesheet>
Как только ресурс обновится, ссылка на него изменится, и он будет загружен заново и закэширован. Естественно, эта схема сброса кэша будет работать и для других заголовков кэширования.
Итоги
Мы рассмотрели все основные заголовки клиентского кэширования и научились использовать их для повышения скорости загрузки сайтов. Ну и конечно, настроили их использование в Angie.
За скобками остались особенности работы кэша в реальных браузерах: разделение на кэш в оперативной памяти и на диске, ограничения на кэш в мобильных устройствах и так далее О таких особенностях полезно знать, но в целом подход к кэшированию они не меняют: мы максимально поощряем кэш статических ресурсов и предполагаем, что ресурс будет сохранён у клиента. Никаких гарантий, впрочем, нет, так что в любом случае нужно оптимизировать размеры ресурсов, использовать сжатие и использовать другие методики оптимизации.