awesome-everything EN
↑ Обратно к восхождению

Кеширование

Stale-while-revalidate: отдать устаревшее сразу, обновить в фоне

Суть SWR разделяет свежесть и задержку на двух слоях — директива HTTP Cache-Control и клиентская модель загрузки данных. Ты меняешь ограниченную устарелость на стёртый пик p99 ревалидации, потому что ни один запрос не блокируется на origin.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 16 min

У API цен стоит max-age=60. p50 latency — 8мс, красота на дашборде. Но каждые 60 секунд запись протухает, и следующий запрос блокируется на 400мс round-trip к origin, чтобы её обновить. На тысячах req/s этот «следующий запрос» — сотни пользователей, каждую минуту, попадающие прямо в твой p99. График — пила: ровно 8мс, потом всплеск 400мс точно на каждой границе TTL. Команда потратила спринт на погоню за «медленным origin», который никогда не был медленным — он просто синхронно ревалидировался в горячем пути живого пользователя.

Пила — это цена синхронной ревалидации

У обычного TTL-кэширования есть одно жестокое свойство: в момент протухания записи кто-то платит полную цену origin за её перезаполнение. С одним max-age этот кто-то — живой пользователь: невезучий запрос, пришедший первым после протухания, блокируется, пока origin не ответит. Твоя установившаяся latency — это время попадания в кэш; твоя tail latency — время промаха; а TTL гарантирует, что ты периодически сэмплируешь промах в реальный трафик. Чем выше частота запросов, тем больше пользователей попадает на каждую границу, тем толще твой p99.

Stale-while-revalidate разрывает связь. Когда запись пересекает окно свежести, но всё ещё внутри окна SWR, кэш делает две вещи разом: возвращает устаревшее значение мгновенно (пользователь получает latency попадания в кэш, ~8мс) и запускает асинхронное обновление к origin. Пользователь, который спровоцировал обновление, никогда его не ждёт. Свежее значение прилетает на несколько сотен миллисекунд позже и обслуживает следующие запросы. Ревалидация всё так же происходит — она просто перестала происходить на переднем плане.

Сделка, которую ты заключаешь, явная и ограниченная: ты соглашаешься, что часть ответов слегка устаревшая, в обмен на удаление пика p99. Устарелость ограничена окном SWR; latency ограничена временем попадания в кэш. Сеньор читает stale-while-revalidate=N как «меня устраивают данные до N секунд за дедлайном свежести, пока ни один человек не ждёт обновление».

RFC 5861: два окна, затем stale-if-error как пол

Это настоящая HTTP-директива, стандартизированная в RFC 5861, и она работает в CDN и в браузерах. Важны два расширения Cache-Control:

  • stale-while-revalidate=N — в течение N секунд после протухания записи отдавай её мгновенно и ревалидируй в фоне.
  • stale-if-error=N — в течение N секунд после протухания, если origin вернул ошибку (500/502/503/504) или недоступен, продолжай отдавать устаревшую копию вместо проброса ошибки.

Боевой заголовок читается как эшелонированная защита:

Cache-Control: max-age=60, stale-while-revalidate=300, stale-if-error=86400

Читай его как три вложенных окна. 0–60с: свежо, отдаётся напрямую. 60–360с: устарело, отдаётся мгновенно, пока в фоне идёт обновление (окно SWR). при сбое origin, до 24ч: всё ещё отдаётся из последней хорошей копии, а не бросает 503 в пользователя (пол SIE). Первое окно оптимизирует свежесть, второе убивает tail latency, третье превращает аварию в «деградировало, но живо».

СтратегияКто платит round-trip к originМакс. устарелостьp99 на границе TTL
только max-age=60Живой пользователь, каждые 60с0 (всегда свежо)Всплеск до latency origin
max-age=60, swr=300Фоновый фетч — без пользователяДо 60+300сРовно (нет промаха на переднем плане)
SWR + stale-if-errorФон; никто во время аварииДо окна SIEРовно, переживает падение origin
Почему это работает

Имена — зеркальные. stale-while-revalidate — инструмент счастливого пути: origin в порядке, ты просто не хочешь, чтобы пользователи ждали обновление. stale-if-error — инструмент пути сбоя: origin болен, и устаревшее лучше, чем 503. Они независимы: можно выкатить один без другого, но их парность — дефолт сеньора, потому что один SWR всё равно отдаст пользователю ошибку, как только origin сломается и окно SWR истечёт.

Та же идея на слой выше: SWR/React Query на клиенте

HTTP-директива живёт в сети. Ровно тот же паттерн живёт в клиентских библиотеках загрузки данных — библиотека SWR буквально названа в его честь, а React Query (TanStack Query) реализует ту же модель. Когда компонент просит ключ, библиотека возвращает закэшированное значение из последнего фетча мгновенно (без спиннера, если ключ уже видела), затем запускает фоновый запрос и подменяет на свежие данные, когда они придут. Пользователь видит мгновенный контент, который тихо сам себя исправляет.

Клиентская модель добавляет два боевых поведения, которых у голого заголовка нет:

  • Коалесинг запросов / single-flight. Если два компонента вызывают useSWR("/api/user") в один тик, SWR делает один запрос, и оба получают один результат. Дефолтный dedupingInterval — 2000мс: повторные вызовы того же ключа в пределах 2с схлопываются на in-flight промис. React Query делает то же; его аналогичный staleTime по умолчанию 0 (рефетч жадно) против 2с у SWR.
  • Триггеры ревалидации. Обе библиотеки по умолчанию ревалидируют при фокусе окна и при восстановлении сети, так что вкладка, оставленная открытой на час, показывает свежие данные в момент, когда ты в неё кликаешь — снова отдано stale-first, обновлено в фоне.

Будь то edge-нода CDN или React-хук, контракт идентичен: верни то, что есть сейчас, забери то, что верно, следующим, никогда не блокируй человека на обновлении.

Выбери лучший вариант

API листинга товаров за CDN видит классическую поминутную пилу p99 от синхронной ревалидации. Данные каталога могут быть устаревшими на несколько минут без вреда. Выбери заголовок.

Где это кусается: стампиды, неограниченная устарелость и auth

SWR не свободен от режимов отказа — он их перемещает. Три кусаются в проде.

Стампид фоновых обновлений. Наивный edge запускает одну фоновую ревалидацию на каждый запрос, который нашёл запись устаревшей. Если популярный ключ протухает, пока он принимает 10k req/s, ты можешь запустить тысячи одновременных обновлений к origin, который ожидал одно, — самонанесённый thundering herd, только теперь это фоновые фетчи плавят origin, а не пользователи видят latency. Фикс — single-flight: первый устаревший запрос запускает ровно одно обновление (Cloudflare помечает его UPDATING), а всех остальных обслуживают устаревшим, пока оно не прилетит. Добавь jitter к таймингу обновления, чтобы много ключей не протухали и не ревалидировались синхронно.

Неограниченная устарелость, когда origin лежит. Это классическая ошибка stale-if-error наоборот. Если origin недоступен, фоновая ревалидация падает — и плохо построенный кэш может тогда продлевать устаревшую копию вечно, «кэш до тепловой смерти». Нужен жёсткий потолок: конечное окно SWR, конечное окно stale-if-error. За их пределами кэш обязан упасть громко, а не отдавать контент неизвестного возраста. Устарелость всегда должна быть ограниченной.

Отдача устаревших auth и прав. Вот этот — опасный. SWR корректен для контента, которому нормально быть на несколько минут старее — список товаров, пост в блоге, плитка аналитики. Он неверен для решений авторизации, фича-флагов, гейтящих доступ, или всего, где устаревшее = регрессия безопасности. Отдай отозванному пользователю устаревшее на 5 минут «у тебя всё ещё есть доступ» — и ты выкатил уязвимость. Правило сеньора: никогда не SWR-ить проверку прав. Персональные, чувствительные к безопасности ответы получают короткую, синхронную, валидированную свежесть — или не кэшируются вовсе.

Викторина

С `Cache-Control: max-age=60, stale-while-revalidate=300` запрос приходит через 90 секунд после кэширования записи. Что получит пользователь?

Викторина

Популярный ключ протухает под тяжёлым трафиком, и твой edge запускает одну фоновую ревалидацию на каждое устаревшее попадание. В чём боевой риск и фикс?

Расставь шаги по порядку

Расставь, что происходит с запросом, пришедшим во время окна stale-while-revalidate:

  1. 1 Кэш обнаруживает, что запись за пределами max-age, но всё ещё внутри окна SWR
  2. 2 Он возвращает устаревшее закэшированное значение пользователю мгновенно — без ожидания origin
  3. 3 Он запускает ровно одну фоновую ревалидацию к origin (single-flight)
  4. 4 Свежий ответ приходит и заменяет закэшированную запись вне полосы
  5. 5 Последующие запросы получают свежее значение (или снова устаревшее, если окно истекло)
Вспомните перед уходом
  1. 01
    Объясни, почему один max-age порождает пилу p99 и как добавление stale-while-revalidate её выравнивает.
  2. 02
    Когда stale-while-revalidate — неверный инструмент, и от каких режимов отказа сеньор обязан защититься?
Итог

Stale-while-revalidate разделяет свежесть и задержку: вместо того чтобы давать живому пользователю платить round-trip к origin каждый раз, когда истекает TTL, кэш возвращает устаревшее значение мгновенно и ревалидирует в фоне, так что пила p99 синхронной ревалидации выравнивается. Это живёт на двух слоях — директива HTTP Cache-Control: stale-while-revalidate=N (RFC 5861, уважаемая CDN и браузерами) и клиентские библиотеки вроде SWR и React Query, которые возвращают последние известные данные мгновенно, коалесят дублирующие запросы через dedupingInterval и ревалидируют при фокусе и реконнекте. Сделка явная: ограниченная устарелость за стёртую tail latency. Сопрягай её с stale-if-error, чтобы авария origin деградировала мягко, а не бросала 503. Затем держи в уме три режима отказа: делай фоновое обновление single-flight, иначе горячий ключ запустит thundering herd; ограничивай устарелость конечными окнами, иначе лежащий origin будет вечно отдавать контент неизвестного возраста; и никогда не применяй SWR к auth, правам или всему, где устаревшие данные — это баг безопасности.

Продолжить восхождение ↑SWR: тест с выбором ответа
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.