Cache busting
Cache busting
Cache busting (сброс кэша через смену адреса) — техника, при которой файлу
при каждом изменении дают новый URL, чтобы браузеры и промежуточные кэши
гарантированно скачали свежую версию, а не отдали сохранённую старую.
История
Проблема, которую решает cache busting, ровесница самого веба. Уже в HTTP/1.0
(RFC 1945, май 1996 года) появился заголовок Expires — сервер мог сказать
браузеру: «храни этот файл у себя и не спрашивай меня до такой-то даты». В
HTTP/1.1 (RFC 2616, июнь 1999-го) добавили более гибкий Cache-Control с
директивой max-age. Кэширование резко снижало нагрузку на сервера и ускоряло
сайты — но породило обратную беду: если файл закэширован на год вперёд, как
доставить пользователю исправление уже сегодня?
Ответ индустрия нащупала в середине 2000-х, когда сайты превратились в тяжёлые
приложения с большими JS- и CSS-файлами. Ключевая фигура здесь — Стив Саудерс
(Steve Souders), руководитель направления производительности в Yahoo, позже в
Google. В книге «High Performance Web Sites» (O'Reilly, 2007) он сформулировал
правило «Add an Expires Header»: статике — максимальный срок кэширования, а
свежесть обеспечивать сменой имени файла. Сам приём он называл «revving» (от
revision — ревизия): style.css превращается в style.v2.css.
Вехи дальше:
- 2008 — Саудерс публикует разбор «Revving Filenames: don't use
querystring»: популярный тогда способstyle.css?v=2ломался на
прокси-кэшах (в частности, Squid по умолчанию не кэшировал URL со знаком
вопроса). Индустрия начала мигрировать на версию в имени файла. - 2011 — Ruby on Rails 3.1 включает asset pipeline (Sprockets): фреймворк
сам считает MD5-хеш содержимого и вписывает его в имя файла
(application-1a2b3c….css). Fingerprinting становится штатной практикой, а
не ручным трюком. - 2012+ — сборщики фронтенда (Webpack Тобиаса Копперса, позже Parcel,
Vite, esbuild) делают контент-хеши в именах файлов дефолтом целой экосистемы. - 2017 — RFC 8246 «HTTP Immutable Responses» (автор — Патрик Макманус из
Mozilla): директиваCache-Control: immutableофициально закрепляет
логику «этот URL никогда не изменится, даже не проверяй». Firefox поддержал
её ещё в 2016-м, во многом под давлением Facebook, страдавшего от лишних
проверочных запросов при каждом обновлении ленты.
Сегодня cache busting — не отдельный продукт, а общепринятый приём, зашитый
почти в каждый инструмент сборки веба.
Что это такое
Идея опирается на одно простое свойство HTTP-кэшей: ключом кэша является
URL. Браузер не заглядывает внутрь файла и не сравнивает содержимое — он
смотрит только на адрес. Если адрес совпал и срок хранения не вышел, файл
берётся с диска, сервер даже не узнает о запросе.
Отсюда два следствия. Плохое: если ты выложил новый widgets.js по старому
адресу, пользователь со свежим кэшем будет гонять старый код, пока кэш не
протухнет — час, день, год, как настроено. Хорошее: если изменить адрес хотя бы
на символ, для кэша это совершенно новый объект, и браузер обязан сходить за
ним на сервер.
Cache busting — это сознательная эксплуатация второго следствия. Каждой версии
файла — свой уникальный URL. Приёма три:
- Query-параметр:
widgets.js?v=8. Проще всего, не требует переименования
файлов на диске, поэтому любим CMS и ручными правками. - Версия в имени файла:
widgets.v8.jsили хеш содержимого
widgets.9f8a3c.js. Надёжнее: адрес меняется «по-настоящему», и любые кэши
обязаны его уважать. - Версия в пути:
/v8/widgets.js. То же самое, но версией управляют на
уровне каталога или CDN-префикса.
Важная пара для различения — cache busting vs инвалидация кэша.
Инвалидация — это «найти все копии старого файла и объявить их недействительными»:
задача, знаменитая своей сложностью (шутку «в информатике есть только две
трудные задачи — инвалидация кэша и придумывание имён» приписывают Филу
Карлтону из Netscape). Cache busting — хитрый обход этой задачи: мы никого не
инвалидируем, старые копии продолжают лежать по старым адресам, просто на них
больше никто не ссылается. Они умрут сами, когда истечёт срок.
Вторая пара — cache busting vs ревалидация (ETag/Last-Modified). Ревалидация —
это когда браузер при каждом обращении переспрашивает сервер: «у меня версия с
таким-то отпечатком, она ещё актуальна?» — и получает либо короткое «304, всё
то же», либо новый файл. Свежесть гарантирована, но каждый ресурс стоит одного
сетевого запроса. Cache busting убирает и эти запросы: раз URL уникален для
содержимого, проверять нечего.
Аналогии из жизни
Номер выпуска журнала. Ты не просишь в киоске «свежий Коммерсантъ вообще»,
ты покупаешь «номер 152». Новый выпуск получает новый номер, и перепутать его
со вчерашним невозможно — само имя гарантирует свежесть. Так и хешированный
файл: имя однозначно определяет содержимое. Где ломается: журнал приходит
к тебе сам, по расписанию, а браузер ничего не скачивает, пока страница не
попросит новый адрес. Если HTML со ссылкой на «номер 152» сам застрял в кэше,
ты так и будешь читать про номер 151 — про это ниже, это главные грабли.
Наклейки с датой на заготовках в ресторане. Повар не нюхает каждый
контейнер (не «ревалидирует»), он смотрит на маркировку: дата свежая — берём.
Быстро и без проверок, как immutable-кэш. Где ломается: наклейку можно
переклеить, не меняя содержимого, — и в вебе так же: поднять ?v=9 без
реальных изменений в файле значит заставить всех пользователей заново скачать
то же самое. Обратная ошибка хуже: поменять содержимое, оставив старую
наклейку, — и никто в мире не узнает, что файл другой.
Номер автобусного маршрута. Если маршрут «7» поехал по другим улицам, а
номер оставили, пассажиры по привычке уезжают не туда. Транспортники в таких
случаях вводят «7к» — новое имя для нового поведения, и путаницы нет. Где
ломается: человек может сесть в «семёрку» по привычке, не глядя на табличку;
браузер, наоборот, абсолютно буквален — он идёт ровно по той ссылке, что
написана в HTML, ни привычек, ни интуиции у него нет. Аналогия занижает
надёжность механизма: в вебе «пересадка на 7к» срабатывает у ста процентов
пассажиров мгновенно.
Как это работает
Разберём канонический продакшен-цикл со сборщиком (Webpack, Vite — неважно).
Шаг 1. Сборка. Сборщик берёт исходный widgets.js, считает хеш его
содержимого (обычно усечённый MD5 или xxHash — криптостойкость тут не нужна,
нужна только чувствительность к изменениям) и кладёт результат в
dist/widgets.9f8a3c1d.js. Изменился хоть один байт исходника — хеш другой,
имя другое.
Шаг 2. Перепрошивка ссылок. Сборщик проходит по HTML-шаблонам и всем
местам, где файл упоминается, и подставляет новое имя:
<script src="/assets/widgets.9f8a3c1d.js">. Часто это делается через
манифест — JSON-карту вида {"widgets.js": "widgets.9f8a3c1d.js"}, которую
читает серверный шаблонизатор.
Шаг 3. Заголовки. Тут ключевая асимметрия, на которой всё держится:
HTML-документ: Cache-Control: no-cache
(хранить можно, но перед использованием — проверить)
Хешированная статика: Cache-Control: max-age=31536000, immutable
(год не трогать и даже не проверять)
HTML — единственная точка входа со стабильным адресом, поэтому он всегда
свежий (ценой одного лёгкого запроса-проверки). А вся тяжёлая статика, на
которую он ссылается, кэшируется намертво — её свежесть гарантирована самим
именем.
Шаг 4. Деплой в правильном порядке. Сначала выкладываем новые файлы
статики (старые не удаляем!), потом — новый HTML. Если сделать наоборот,
возникает окно, в котором свежий HTML просит widgets.9f8a3c1d.js, а его на
сервере ещё нет — пользователь получает 404 и сломанную страницу. Старые файлы
держат ещё какое-то время, потому что у кого-то в открытой вкладке живёт старый
HTML, ссылающийся на них.
Шаг 5. Первый визит пользователя после релиза. Браузер проверяет HTML
(запрос-проверка, сервер отвечает новой версией), видит незнакомое имя скрипта,
скачивает его, кэширует на год. Все последующие визиты: HTML проверился одним
дешёвым запросом, статика поднялась с диска за миллисекунды, сеть не тронута.
С query-параметром (widgets.js?v=8) схема та же, только версию проставляет
человек или CMS, а не хеш-функция. Это работает, но держится на дисциплине:
забыл поднять номер — изменение молча не доехало.
Query-string — способ с оговорками
Часть промежуточных кэшей исторически игнорировала или не кэшировала URL с
?, а некоторые CDN умеют настраиваться «не учитывать query при построении
ключа» — тогда ?v=8 вообще перестаёт что-либо ломать. Если контролируешь
сборку — клади версию в имя файла. Query-вариант оправдан, когда файлы
нельзя переименовывать (чужая CMS, ручной деплой).
Это не очистка кэша, а отказ от борьбы с ним
Cache busting никого не «сбрасывает». Старые копии спокойно доживают свой срок во всех кэшах планеты — просто на них больше не указывает ни одна ссылка. Знаменитая трудная задача инвалидации решается тем, что её перестают решать.
Где встречается в обычной жизни
- «Очистите кэш и обновите страницу» — совет техподдержки, знакомый
каждому. Это ручной cache busting со стороны пользователя: сайт не позаботился
о смене адресов, и стряхивать старьё приходится самому (Ctrl+Shift+R — та
самая «жёсткая перезагрузка», которая заставляет браузер перекачать всё). - Сайт «полуобновился»: текст новый, а вёрстка поехала. Классический
симптом: HTML доехал свежий, а CSS у тебя из кэша недельной давности, потому
что адрес стилей не поменяли. - Обновление приложения на телефоне устроено по той же логике: каждая
версия — отдельный артефакт со своим номером, старая остаётся на устройстве,
пока новая не установлена целиком. Никто не «патчит файл по старому адресу». - Аватарка в мессенджере обновилась не у всех — у кого-то ещё держится
кэшированная картинка по старому URL; когда сервис меняет ссылку на новую
(с другим хешем), обновление долетает мгновенно. - Ценник со старой ценой на кассе — жизненный аналог рассинхрона кэшей:
на полке (кэш) одно, в базе (сервер) другое, и до «ревалидации» полки ты
видишь устаревшие данные.
Где встречается в IT и бизнесе
- Каждый релиз фронтенда. Любой сайт со сборщиком выкатывается через
контент-хеши. Это настолько стандарт, что отсутствие cache busting в
продакшене — маркер самодельного или очень старого процесса. - Виджеты и встраиваемые скрипты на чужих сайтах. Ты раздаёшь клиентам
<script src="…/widget.js">— адрес зафиксирован в их HTML навсегда. Внутри
этого файла-загрузчика обычно лежит подгрузка основного кода уже с версией:
так обновляешь виджет у всех, не прося никого править вставку. - Email-рассылки и лендинги кампаний: картинки и стили кэшируются
агрессивно (в том числе прокси почтовых сервисов), исправить уехавший баннер
можно только новым URL. - A/B-тесты и постепенные выкатки: две версии бандла живут параллельно под
разными хешированными именами, и это не конфликтует — у каждой свой адрес. - Мобильные и десктопные гибриды (WebView-приложения): внутри них
веб-кэш ещё цепче браузерного, и без версионированных URL «горячее»
обновление интерфейса просто не доставляется.
Кто пользуется
Практически весь крупный веб, различия только в деталях:
- Google раздаёт статику с отдельных доменов (
gstatic.com) с годовым
max-age; адреса ресурсов версионируются, сами файлы неизменяемы. - Facebook — один из инициаторов
Cache-Control: immutable: по их данным
времён 2015–2016 годов, огромная доля запросов при перезагрузке ленты была
бессмысленными ревалидациями уже актуальных файлов; хешированные имена плюс
immutableэти запросы убрали. - Rails-экосистема (GitHub, Shopify, Basecamp) живёт на fingerprinting из
коробки со времён Rails 3.1. - Wikipedia версионирует ресурсы через ResourceLoader — параметры версии в
URL модулей. - CDN-провайдеры (Cloudflare, Akamai, Fastly) строят на этом тарифы и
архитектуру: хешированную статику можно кэшировать на границе (edge)
бесконечно, и это самый дешёвый трафик в индустрии.
Точных публичных цифр «сколько процентов сайтов используют cache busting» я не
встречал — честно, не знаю; косвенно масштаб виден по тому, что контент-хеш в
имени файла — дефолт всех основных сборщиков.
Альтернативы и конкуренты
- Ревалидация через ETag / Last-Modified.
Плюс: URL стабильные, свежесть проверяется автоматически, ничего не надо
версионировать.
Минус: каждый ресурс — лишний сетевой запрос при каждом визите (пусть и с
коротким ответом 304); на странице с десятками ресурсов это заметная
задержка. - Короткий max-age (минуты).
Плюс: примитивно просто, никакой сборки.
Минус: окно несвежести всё равно есть, а трафик и нагрузка растут; это
компромисс «плохо и там, и там». - Целевая очистка CDN (purge по URL или тегу).
Плюс: мгновенно убирает старьё с граничных серверов, URL не меняются.
Минус: не дотягивается до браузерных кэшей пользователей — то, что уже
скачано на устройства, так и останется там до истечения срока. - Service Worker со своей стратегией кэширования.
Плюс: полный программный контроль (офлайн, предзагрузка, свои правила).
Минус: заметная сложность и собственный класс багов; устаревший service
worker сам становится кэш-проблемой, которую приходится «бастить».
На практике это не конкуренты, а слои: хеши в именах + immutable для статики,
no-cache с ревалидацией для HTML, purge для CDN-инцидентов.
Когда НЕ стоит использовать
- Для самих HTML-страниц. Адрес страницы — это то, что люди сохраняют в
закладки и передают друг другу; менять его при каждом релизе нельзя. Поэтому
HTML не «бастят», а кэшируют коротко или с обязательной проверкой. - Для API-ответов. Версия в URL API (
/v1/,/v2/) — про совместимость
контрактов, а не про свежесть данных. Свежестью динамических ответов
управляют заголовками кэширования и ETag; приделывать к запросам случайный
?_=123456, чтобы «не кэшировалось», — антипаттерн, который просто убивает
кэш целиком. - Руками, без автоматизации, на живом продукте. Ручное
?v=7держится
ровно до первого забытого инкремента: файл обновлён, версия старая,
изменение молча не доехало до части пользователей — и такие баги почти
невозможно воспроизвести у себя. Если сборки нет и не будет, честнее
поставить короткий max-age.
Связанные понятия
- HTTP-кэширование (Cache-Control) — система заголовков, управляющая тем,
кто, что и как долго может хранить; фундамент всей темы. - ETag — «отпечаток» версии ресурса для условных запросов и ответов 304.
- CDN (Content Delivery Network) — сеть граничных кэшей по всему миру;
главный бенефициар неизменяемых URL. - Хеш-функция — математика, превращающая содержимое файла в короткий
отпечаток; чувствительность к каждому байту и делает fingerprinting честным. - Immutable (RFC 8246) — директива «даже не проверяй», логичное завершение
идеи версионированных адресов. - Service Worker — программируемый кэш-посредник в браузере, следующий
уровень управления доставкой.
Литература и источники
- Steve Souders, «High Performance Web Sites» (O'Reilly, 2007, en) — та самая
книга с 14 правилами; глава про Expires-заголовок — прямо об этом. - MDN Web Docs, раздел «HTTP caching» — лучший современный обзор:
https://developer.mozilla.org/ru/docs/Web/HTTP/Caching - RFC 9111 «HTTP Caching» (2022, en) — действующая спецификация:
https://www.rfc-editor.org/rfc/rfc9111 - RFC 8246 «HTTP Immutable Responses» (2017, en):
https://www.rfc-editor.org/rfc/rfc8246 - Статья Саудерса «Revving Filenames: don't use querystring» (2008, en) —
искать в Google по точному названию. - web.dev (официальный ресурс Google) — материалы «Love your cache» и «HTTP
cache» — искать «web.dev http cache».
Где встретилось у меня
Вчера дорабатывал сайт жилого комплекса: виджеты каталога и генплана прошли
восемь итераций за день, и каждая выкатка на тестовый сервер шла с новым
номером версии в URL скриптов и стилей — иначе проверяющий браузер раз за
разом подтягивал бы закэшированную старую версию, и было бы непонятно, что
именно тестируешь. Классический ручной cache busting через ?v=.
Краткое резюме
- Ключ браузерного кэша — URL: новый адрес означает обязательное скачивание,
старый — риск получить залежавшуюся копию. - Cache busting не чистит кэши, а обходит задачу инвалидации: каждой версии
файла — уникальное имя, старые копии умирают сами. - Золотая схема: HTML —
no-cacheсо стабильным адресом; статика — хеш
содержимого в имени плюсmax-age=31536000, immutable. - Query-параметр
?v=работает, но хуже смены имени файла: часть кэшей
учитывает query по-своему, а ручной инкремент версии легко забыть. - Порядок деплоя важен: сначала новая статика, потом новый HTML, старую
статику не удалять сразу.