Как настроить Last-Modified, ETag и revalidate в 2026

Если у сайта в 2026 году до сих пор не настроены Last-Modified, ETag и нормальная логика revalidate, то он почти наверняка отдаёт лишние байты и теряет время на повторных запросах. Я на своей практике регулярно вижу одну и ту же картину: проект на WordPress или Bitrix крутится на PHP 8.2, MySQL 8.0, CDN подключён, а заголовки либо отсутствуют, либо настроены криво и только мешают.

Грубо говоря, эти три механизма — не магия, а способ честно договориться с браузером: «если страница не менялась, не тащи её заново». И это очень полезно не только для скорости, но и для экономии нагрузки на сервер, особенно когда у вас каталог, блог, фильтры, личный кабинет или сложная интеграция с CRM. Если нужна доработка сайта под такую задачу, я обычно начинаю именно с заголовков кэширования, а уже потом лезу в оптимизацию картинок, Redis и CDN.

Что такое Last-Modified, ETag и revalidate

Начну с простого. Last-Modified — это дата последнего изменения ресурса. Браузер или прокси запоминают её и при следующем запросе спрашивают сервер: «А страница с тех пор изменилась?». Если нет, сервер может ответить 304 Not Modified, и тогда тело страницы не передаётся заново.

ETag — это уже не дата, а идентификатор версии ресурса. Обычно сервер считает хэш или отдаёт внутренний fingerprint. Смысл тот же: клиент хранит ETag и в следующий раз присылает его обратно через If-None-Match. Если совпало — снова 304. На деле ETag удобен, когда дата изменения не отражает реальную свежесть данных, например при сборке страницы из нескольких источников.

revalidate — это не отдельный заголовок сам по себе, а поведение проверки актуальности кэша. Чаще всего мы говорим про Cache-Control: no-cache, must-revalidate или про сочетание max-age с условной валидацией через Last-Modified/ETag. И вот тут многие путаются: no-cache не значит «не кэшировать вообще». Это плохая идея понимать так буквально. В реальности браузер может хранить ответ, но обязан перепроверять его у сервера.

ℹ️
Коротко по смыслу: Last-Modified — дата, ETag — версия, revalidate — повторная проверка у сервера перед использованием кэша. Если на сайте мало динамики, это даёт заметный выигрыш по скорости и нагрузке.

У одного клиента на Bitrix была странная проблема: главная открывалась быстро, но PageSpeed Insights показывал просадки по TTFB и лишние запросы на проверку HTML. Оказалось, что Nginx вообще не отдавал Last-Modified для HTML-страниц, а ETag включался только для статики. После настройки заголовков и нормального Cache-Control количество повторных загрузок страницы в браузере реально упало, а у части страниц время ответа снизилось почти на 20-25% по ощущениям пользователей. Да, это не «сделает сайт в 10 раз быстрее», но в сумме очень полезно.

Как это работает в браузере, CDN и прокси

Когда пользователь открывает страницу впервые, сервер отдаёт HTML вместе с заголовками Last-Modified и/или ETag. Браузер сохраняет ответ в кэше. При следующем заходе он не обязательно скачивает всю страницу заново. Он может отправить условный запрос:

If-Modified-Since: Wed, 31 Jan 2026 12:10:00 GMT
If-None-Match: "a1b2c3d4e5"

Если ресурс не изменился, сервер возвращает 304. Это значит: тело ответа не передаём, трафик экономим, браузер быстро берёт копию из локального кэша. И вот тут важно понимать: 304 — это не ошибка. Это нормальный, хороший ответ.

Но есть нюанс. Если у вас стоит CDN, обратный прокси или nginx cache, то проверка может происходить на разных уровнях. Я обычно смотрю цепочку так: браузер → CDN → Nginx → приложение на PHP 8.1/8.2/8.3 → база MySQL 5.7/8.0. Если на каком-то уровне заголовки ломаются, результат получается непредсказуемый. Например, CDN может сохранить ETag от origin, но потом origin пересобирает HTML с другим форматированием, и заголовок начинает врать. В таких случаях ETag превращается в источник лишней головной боли.

Если проект большой, я советую не ограничиваться одной настройкой в CMS. Иногда нужна комплексная проверка сайта: заголовки, серверный кэш, логика редиректов, CDN, заглушки на 304 и поведение при обновлении шаблонов. На практике это экономит больше времени, чем точечная правка одного файла.

⚠️
Не путайте 304 с «ошибкой кэша»: если сервер честно отвечает Not Modified, это хорошо. Плохо, когда 304 отдаётся для уже изменившегося HTML или когда ETag генерируется нестабильно и ломает кеширование на стороне CDN.

Когда ставить Last-Modified, а когда ETag

Я обычно начинаю с вопроса: что именно мы кэшируем? Если это статичный файл — CSS, JS, SVG, шрифты, изображения — тут Last-Modified и ETag работают отлично. Если это HTML страницы с частым обновлением, всё уже зависит от структуры проекта.

Last-Modified хорош, когда у ресурса есть понятная дата изменения. Например, файл в файловой системе, статья в блоге, страница с публикацией, которая обновляется раз в сутки. Но если страница собирается динамически из нескольких сущностей — товар, остаток, цена, доставка, промо-баннер, блок рекомендаций — дата изменения может врать. Страница вроде бы «не менялась», а пользователь увидел другой контент. В таком случае лучше полагаться на ETag или вообще на серверный кэш с нормальным TTL.

ETag удобно использовать, когда вы можете стабильно и дешево вычислить отпечаток ответа. Но вот честно говоря, для HTML на высоконагруженном сайте я часто отключаю ETag, если за ним стоит CDN или кластер. Почему? Потому что некоторые прокси и балансировщики начинают генерировать несовместимые ETag или сравнивать их не так, как ожидается. В результате получаем лишние 200 вместо 304, а иногда и странные проблемы после деплоя.

Для статики ETag я оставляю чаще, чем для HTML. Для HTML — по ситуации. Для API — почти всегда отдельная история, там чаще нужен явный контроль через versioning и Cache-Control, а не слепая вера в автоматические заголовки.

💡
Практическое правило: для статичных файлов оставляйте Last-Modified и ETag, для HTML смотрите на архитектуру. Если есть CDN и много динамики, ETag часто лучше отключить и работать через Cache-Control + revalidate.

Настройка на Nginx: рабочий пример для 2026 года

На сервере с Nginx, PHP-FPM 8.2 и MariaDB/MySQL 8.0 я обычно начинаю с базовой логики для статики. Если сайт без экзотики, это уже даёт хороший эффект. Ниже пример, который можно адаптировать под свой проект.

server {
    listen 443 ssl http2;
    server_name example.ru www.example.ru;
    root /var/www/example.ru/public;

    # Статика
    location ~* \.(?:css|js|mjs|png|jpg|jpeg|gif|webp|avif|svg|ico|woff|woff2|ttf|otf)$ {
        expires 30d;
        add_header Cache-Control "public, max-age=2592000, immutable";
        add_header Vary "Accept-Encoding" always;
        access_log off;
    }

    # HTML и динамика
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

        fastcgi_cache_bypass $http_cache_control;
        fastcgi_no_cache $http_pragma $http_authorization;
    }

    # Условная валидация для HTML
    location = / {
        add_header Cache-Control "no-cache, must-revalidate" always;
    }
}

Тут я специально показываю не только Last-Modified и ETag, но и Cache-Control. Потому что без него вся конструкция часто работает криво. Для HTML мы не говорим браузеру «держи это месяц». Мы говорим: «можно хранить, но проверяй актуальность». И это как раз та самая revalidate-логика.

Если говорить про Last-Modified на уровне Nginx, у статических файлов он обычно берётся автоматически из файловой системы. А вот ETag зависит от сборки и настроек. В некоторых конфигурациях я вообще отключаю ETag на уровне Nginx для HTML, чтобы не было лишней рассинхронизации с upstream. Это особенно актуально, если у вас несколько серверов за балансировщиком и деплой идёт через CI/CD.

И ещё момент. Не надо бездумно включать immutable на HTML. Это ошибка. immutable хорош для версионированной статики вроде style.9f2b3.css или app.a12cde.js. Для HTML — забудьте про это. Страница может измениться хоть через минуту, и браузер должен уметь её перепроверить.

Настройка в PHP, Bitrix, WordPress и Laravel

На уровне приложения контроль над Last-Modified и ETag иногда нужен даже больше, чем на уровне веб-сервера. Особенно если вы строите страницу сами, а не отдаёте её как готовый файл. Я часто встречаю это в Laravel-проектах, интернет-магазинах на Bitrix и контентных сайтах на WordPress.

Ниже рабочий PHP-пример, который можно использовать как основу. Он ставит Last-Modified по дате обновления записи и отвечает 304, если клиент прислал свежий If-Modified-Since. Для ETag логика похожая.

<?php
$updatedAt = '2026-01-31 12:10:00';
$timestamp = strtotime($updatedAt);
$lastModified = gmdate('D, d M Y H:i:s', $timestamp) . ' GMT';

$etag = '"' . sha1($updatedAt . '|' . ($_SERVER['REQUEST_URI'] ?? '/')) . '"';

header('Cache-Control: no-cache, must-revalidate');
header('Last-Modified: ' . $lastModified);
header('ETag: ' . $etag);

$ifNoneMatch = $_SERVER['HTTP_IF_NONE_MATCH'] ?? '';
$ifModifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? '';

if ($ifNoneMatch === $etag || strtotime($ifModifiedSince) >= $timestamp) {
    header('HTTP/1.1 304 Not Modified');
    exit;
}

echo '<html><body>...</body></html>';

Конечно, это упрощённый вариант. В реальном проекте я обычно добавляю учёт языка, версии шаблона, категории товара, состояния корзины и авторизации пользователя. Потому что если страница отличается для гостя и для авторизованного, ETag должен учитывать это разделение. Иначе можно получить кэш-коллизию, а это уже плохо.

В WordPress я чаще всего настраиваю такое через плагины кэширования и через конфиг сервера, а не через лобовую правку темы. У Bitrix есть свои особенности: там часто важнее корректно настроить кеш компонентов, tagged cache и композитный режим. Я писал об этом отдельно в статье Настройка кеширования в Битрикс: полное руководство. Если у вас Bitrix и проект живой, то Last-Modified и ETag должны работать в связке с кешем компонентов, а не вместо него.

Для WordPress я обычно сначала смотрю, чем кэшируется сайт: LiteSpeed Cache, WP Rocket, W3 Total Cache, Redis, nginx fastcgi_cache. И уже потом решаю, где лучше выставлять заголовки. Иногда достаточно сервера и CDN. Иногда нужно ещё и аккуратно править hooks в теме или mu-plugin. Если нужен сопровождающий WordPress-специалист, я бы именно так и делал: сначала замер, потом настройка, потом повторная проверка.

revalidate, Cache-Control и ответы 304

Вот здесь обычно и начинается путаница. Люди настраивают Last-Modified, а потом удивляются, что страница всё равно скачивается заново. Или наоборот: ставят слишком агрессивный Cache-Control, а потом сталкиваются с устаревшим контентом. По опыту, правильная связка выглядит так:

Когда браузер получает no-cache, он может хранить копию, но перед использованием обязан проверить её актуальность. Это и есть revalidate в практическом смысле. Если сервер отвечает 304, клиент использует локальную копию. Если отвечает 200, значит контент изменился, и новый ответ будет сохранён в кэше.

Я на практике часто использую такую схему для новостных разделов, корпоративных сайтов и блогов. Страница статьи хранится в кэше, но проверяется при каждом открытии. В итоге пользователь не ждёт полную загрузку, а сервер не гоняет лишний HTML. На сильных проектах разница по нагрузке очень заметна: CPU падает, PHP-FPM меньше занят, MySQL получает меньше бесполезных запросов.

И ещё нюанс. Если у вас включён Redis или Memcached, это не отменяет смысл Last-Modified и ETag. Это просто разные уровни оптимизации. Redis ускоряет генерацию страницы, а условная валидация помогает не гонять одинаковый ответ туда-сюда. Я обычно делаю и то, и другое, особенно если проект на Laravel или WordPress и трафик уже не маленький. Про базовый подход к Redis я отдельно писал в статье Настройка Redis для сайта: ускорение WordPress, Bitrix и Laravel.

Типичные ошибки и почему они ломают кэш

Самая частая ошибка — ставить ETag, но не понимать, кто именно его генерирует. Если в цепочке есть CDN, обратный прокси и несколько серверов приложений, одинаковый URL может получать разные ETag на разных нодах. Итог простой: условная проверка перестаёт работать, а браузер получает 200 вместо 304.

Вторая ошибка — ставить Last-Modified по времени деплоя, а не по времени изменения контента. Тогда после выкладки любой мелочи весь сайт становится «новым» для клиента, и кэш начинает работать хуже, чем мог бы. Я однажды видел Bitrix-проект, где дата последней модификации всех страниц обновлялась при каждом ночном cron-задании. Это было просто безумие. Кэш практически не помогал.

Третья ошибка — забывать о персонализации. Если страница отличается для авторизованного пользователя, админа, гостя, мобильного и десктопного варианта, нельзя бездумно ставить один ETag на всех. Это плохая идея. Нужно разделять кэш по cookie, по заголовкам, по сегментам или вообще исключать персонализированные страницы из общего кэша.

Четвёртая ошибка — полагаться только на CMS-плагины. На WordPress часто ставят плагин кэша и думают, что на этом всё. А потом оказывается, что Nginx ещё и сам кэширует, CDN кэширует по своим правилам, а браузеру прилетает мусорная комбинация заголовков. На деле лучше один раз собрать нормальную схему, чем потом месяц разбирать конфликт между LiteSpeed Cache, Cloudflare и серверным прокси.

⚠️
Не смешивайте слишком много слоёв без карты логики: если CDN, Nginx cache, Redis, плагин кэша и PHP-логика работают одновременно, обязательно проверьте, кто отдаёт Last-Modified и ETag. Иначе вы сами себе создадите «вечный 200» или «ложный 304».

Как проверить, что всё работает правильно

После настройки я всегда проверяю не только браузер, но и серверные заголовки через curl. Это быстрее и честнее. Вот базовая команда:

curl -I https://example.ru/
curl -I -H 'If-Modified-Since: Wed, 31 Jan 2026 12:10:00 GMT' https://example.ru/
curl -I -H 'If-None-Match: "a1b2c3d4e5"' https://example.ru/

Если всё настроено правильно, при совпадении данных вы увидите 304 Not Modified. Если сервер снова отдаёт 200 с полным HTML, значит либо заголовки не участвуют в логике, либо приложение игнорирует условные запросы. Тогда я уже иду смотреть Nginx, PHP, плагины кэша, CDN и логи.

В DevTools тоже полезно смотреть вкладку Network. Там видно, какой ресурс был загружен заново, а какой пришёл из disk cache или memory cache. Но DevTools не всегда дают полную картину, особенно если есть CDN. Поэтому я обычно сверяю и curl, и заголовки ответа, и поведение браузера в режиме инкогнито.

Если хочется идти системно, а не на глаз, я бы начинал с технического аудита. У меня на webfull.ru это часто идёт вместе с калькулятором стоимости сайта или отдельной оценкой работ: сначала понимаем объём, потом фиксируем, где именно теряется кеширование, и уже после этого вносим правки. Так и быстрее, и дешевле, чем бесконечно тыкать настройки вслепую.

Что это даёт в 2026 году на реальных проектах

В 2026 году требования к скорости стали жёстче. Пользователь не ждёт 3-4 секунды ради статьи, каталога или карточки товара. Google PageSpeed Insights и Lighthouse тоже не прощают лишних сетевых запросов. И хотя Last-Modified, ETag и revalidate не решают всё, они отлично дополняют HTTP/2, HTTP/3, Brotli, CDN, lazy loading и оптимизацию изображений.

На одном проекте с каталогом на WordPress и WooCommerce после настройки заголовков и отключения лишнего ETag на HTML мы увидели заметное снижение повторных загрузок страниц. В Lighthouse показатели по сети стали ровнее, а на мобильных устройствах с 4G поведение сайта стало предсказуемее. Это не тот случай, когда можно сказать «вау, было 42, стало 98». Но общая картина стала лучше, и это было видно по логам.

На Bitrix я чаще всего вижу пользу в экономии на сервере. Когда компоненты и кеш уже настроены, правильная валидация через Last-Modified помогает не гонять тяжёлые страницы без нужды. Особенно это важно на сайте с высокой посещаемостью, где каждая лишняя генерация HTML — это дополнительные SQL-запросы, лишняя нагрузка на PHP-FPM и больше шансов поймать 502 в пиковые часы. Про такие проблемы у меня есть отдельный разбор в статье Как исправить 502 Bad Gateway и ошибки прокси на сайте.

И вот мой честный вывод: если у вас сайт живёт на PHP 8.2 или 8.3, работает на нормальном Nginx и есть хотя бы минимальный трафик, настройка Last-Modified, ETag и revalidate однозначно стоит времени. Это не косметика. Это базовая гигиена производительности. А если проект уже сложный — интернет-магазин, портал, мультисайт или связка с CRM — тогда без этого вообще нельзя.

Рабочая схема, которую я бы использовал без лишней философии

Если совсем коротко, я обычно делаю так. Для статических файлов — долгий кэш, immutable, Last-Modified и ETag. Для HTML — короткий или условный кэш, обязательная revalidate-логика и аккуратная работа с 304. Для персонализированных страниц — отдельные правила, без попытки «закэшировать всё подряд».

Если проект на Bitrix, WordPress или Laravel, я не ограничиваюсь одним уровнем. Проверяю сервер, приложение, CDN, Redis, плагин кэша и только потом считаю, что задача закрыта. И да, если у сайта много узких мест, одна доработка сайта по заголовкам не спасёт всё целиком, но часто именно с неё и начинается заметное ускорение.

В реальной работе я бы советовал сначала включить диагностику, посмотреть заголовки, проверить 304, убедиться, что кэш не врёт, и только потом расширять схему на весь проект. Если нужна аккуратная поддержка Bitrix или настройка WordPress без хаоса, так и делаю: пошагово, с логами, с curl, с тестом в браузере и с проверкой после деплоя.

И ещё момент. Не надо пытаться сделать всё «идеально» с первого раза. Лучше настроить простую и честную схему, которая стабильно работает на Nginx + PHP 8.2 + MySQL 8.0, чем собрать слишком умную конфигурацию, которую никто не сможет поддерживать через полгода. На практике именно простые решения живут дольше.

Хотите настроить кэширование без ошибок?

Поможем внедрить Last-Modified, ETag и revalidate так, чтобы сайт быстрее работал и корректно обновлял контент.

П
Павел
Веб-разработчик · 10+ лет опыта · Bitrix, WordPress, Laravel

Читайте также

Настройка лимитов памяти PHP и MySQL на сайте в 2026 году Бэкапы сайта: как делать правильно и не терять данные Как настроить alt-тексты для изображений на сайте в 2026 году