vyvchy
    Теми розділу

    03 · Веб і мережі для AQA

    Кешування

    Зміст

    «Я бачу стару версію» — речення, після якого команди витрачають години не туди. не знаходить нову кнопку, тест зелений на вчорашньому бандлі, у колеги все свіже, а у вас ні. Причина зазвичай не в застосунку: між браузером і сервером стоїть кілька сховищ, і кожне має власного господаря й власні правила.

    Ця глава — повний виклад теми на сайті, інші глави посилаються сюди по механіку кешу. Мета — одне вміння: за хвилину сказати, з якого рівня прийшла відповідь і чому.

    Хто кешує і на якій підставі

    (HTTP caching) — це зберігання вже отриманих відповідей у проміжному сховищі, щоб наступного разу не тягнути їх мережею повторно. Під правила підпадає не лише статика: відповідь API — така сама HTTP-відповідь (їхні формати розбирає глава «REST API та формати даних»).

    Головне для тестувальника: браузер сам не вирішує, що і як довго зберігати — правила диктує сервер заголовками відповіді. Тому поведінка кешу не «оптимізація десь усередині», а частина контракту між бекендом і фронтендом: її перевіряють на заголовок.

    Клас кешуКого обслуговуєДе живе
    Приватний (private)одного користувачазазвичай усередині клієнта — браузера
    Спільний (shared)кількох користувачівзазвичай у проміжному вузлі — проксі, CDN

    Спільні кеші додатково діляться на -кеші й керовані — reverse proxy, CDN і Service Worker разом із Cache API розгортають самі розробники сервісу. І контрінтуїтивне наостанок: навіть без Cache-Control відповідь за певних умов може бути збережена й перевикористана — це евристичне кешування.

    Життєвий цикл запиту: у якій точці кеш перехоплює

    Повний шлях запиту розбирає глава «Життєвий цикл запиту»: DNS, встановлення зʼєднання, надсилання, очікування сервера, відповідь, рендер. Кеш вклинюється в нього у трьох точках, і кожна здатна обірвати маршрут раніше.

    Код сторінки

    Service Worker
    Cache API

    HTTP-кеш браузера
    приватний

    Посередник
    проксі, шлюз, CDN

    origin-сервер

    Код сторінки

    Service Worker
    Cache API

    HTTP-кеш браузера
    приватний

    Посередник
    проксі, шлюз, CDN

    origin-сервер

    Service Worker стоїть найближче до сторінки — за означенням це проксі між вебзастосунком, браузером і мережею. Далі приватний HTTP-кеш браузера: якщо копія придатна, мережею не йде нічого. Ще далі — спільні кеші в проміжних вузлах. Тому діагностика починається з визначення рівня, а не з коду застосунку.

    Заголовки, якими сервер керує кешем

    Основний важіль — Cache-Control із директивами через кому, наприклад Cache-Control: public, max-age=3600.

    ДирективаЩо означає
    max-age=Nскільки секунд відповідь свіжа
    s-maxage=Nте саме, але лише для спільного кешу
    no-cacheзберігати можна, перевикористати без валідації — ні
    no-storeне зберігати жодної частини запиту чи відповіді
    must-revalidateзастарілу відповідь без валідації не віддавати
    privateспільний кеш зберігати не повинен, приватний може
    publicзнімає заборону зберігати — для будь-якого кешу

    Термін свіжості береться за першим збігом: s-maxage (лише для спільного кешу), далі max-age, далі Expires мінус Date. Звідси відповідь на класичне питання: коли є і max-age, і Expires, перемагає відносний max-age. Якщо директиви конфліктують — кеш виконує найсуворішу.

    no-cache не означає «не кешувати» — це головна плутанина теми. Копія лягає в кеш, просто перед кожним використанням її валідують на сервері: трафік економиться (тіло не передається, якщо нічого не змінилось), а мережевий похід відбувається щоразу. no-store — інша річ: зберігати заборонено взагалі, і не економиться нічого. Окремо про безпеку: no-store не є механізмом приватності, і RFC каже це прямо — «This directive is not a reliable or sufficient mechanism for ensuring privacy».

    Формула «no-cache — це те саме, що max-age=0, must-revalidate» правдива лише двоскладово. За MDN еквівалентність є: «It is often stated that the combination of max-age=0 and must-revalidate has the same meaning as no-cache». Там же сказано, що таке вживання max-age=0 — спадок реалізацій до HTTP/1.1, які не вміли обробляти no-cache, і що «there's no reason to ever use» цю комбінацію тепер. RFC 9111 директиви не ототожнює: тотожність — за MDN, не за специфікацією.

    Свіжість проти валідації: умовний запит і 304

    Відповідь у кеші живе у двох станах. Поки її вік не перевищив термін свіжості, вона свіжа (fresh) — її віддають без походу в мережу. Після цього вона застаріла (stale), і перед використанням кеш має перепитати сервер. Ця перевірка — валідація, або ревалідація.

    Механізм — умовний запит: кеш надсилає валідатор, тобто значення, що описує версію ресурсу. ETag їде в If-None-Match, Last-Modified — в If-Modified-Since. ETag непрозорий: він ідентифікує версію, і розбирати його на частини не можна. Якщо версія збіглася, сервер відповідає 304 Not Modified — збережену відповідь можна оновити метаданими й перевикористати, тіло повторно не передається.

    У мережевій панелі цей обмін виглядає так. Сервер раніше віддав ресурс із ETag: "33a64df5c1e9", свіжість минула — і кеш повертається з тим самим значенням:

    GET /app.js HTTP/1.1
    Host: example.com
    If-None-Match: "33a64df5c1e9"

    Версія та сама, тож відповідь приходить без тіла:

    HTTP/1.1 304 Not Modified
    ETag: "33a64df5c1e9"
    Cache-Control: max-age=3600

    Ні

    Так

    Так

    Ні

    Так

    Ні

    Потрібна відповідь

    Копія в кеші є?

    Запит до origin-сервера

    Свіжа?

    Віддати з кешу,
    мережею не йде нічого

    Умовний запит
    з валідатором

    304 Not Modified?

    Ні

    Так

    Так

    Ні

    Так

    Ні

    Потрібна відповідь

    Копія в кеші є?

    Запит до origin-сервера

    Свіжа?

    Віддати з кешу,
    мережею не йде нічого

    Умовний запит
    з валідатором

    304 Not Modified?

    Одна межа: ревалідація не гарантована для навігації по історії — наприклад, кнопкою «Назад»; при використанні bfcache (back/forward cache) браузер відновлює знімок сторінки без ревалідації. Тому сценарій «повернувся назад — маю побачити свіжі дані» перевіряють окремо: з no-cache він не випливає.

    Спільний кеш проти приватного: private, public, s-maxage

    Тут ховається найдорожча помилка теми — не , а інцидент безпеки: персоналізована відповідь, що потрапила у спільний кеш, може поїхати іншому користувачу.

    Розмежовує кеші саме private: спільний кеш таку відповідь зберігати MUST NOT, приватний — MAY. Є й кваліфікована форма з переліком імен полів: тоді спільному кешу заборонено зберігати лише названі заголовки, а решту повідомлення — можна.

    А public — це не «дозвіл спільним кешам», хоч так її переказують постійно: «The public response directive indicates that a cache MAY store the response even if it would otherwise be prohibited… In other words, public explicitly marks the response as cacheable». Директива знімає заборону для будь-якого кешу, а спільний кеш із заголовком Authorization — приклад у джерелі, а не зміст директиви. s-maxage задає окремий термін свіжості саме для спільних кешів: так тримають різні строки для CDN і для браузера.

    Два заголовки для дебагу. Age — оцінка того, скільки секунд минуло від моменту, коли відповідь згенерували або ревалідували на origin-сервері, а не «скільки вона пролежала у спільному кеші». Vary додає названі заголовки запиту до ключа кешу, тож надто широкий Vary (класика — Vary: User-Agent) роздуває кеш.

    Наша практика (не канон). Провайдерські заголовки на кшталт X-Cache: HIT чи CF-Cache-Status канонічного джерела не мають: назви й формат залежать від постачальника, тож звіряйтеся з його документацією.

    CDN як спільний кеш і що це змінює для тестування

    CDN (Content Delivery Network) — мережа проміжних серверів ближче до користувачів; вона кешує відповіді й знижує навантаження на origin-сервер, бо частина запитів на статику до нього просто не доходить. З погляду HTTP це звичайний спільний кеш. Для тестування зʼявляється рівень, на який ваш код не має впливу: кеш браузера можна вимкнути, — зняти, а спільний кеш живе на чужій машині в чужому регіоні.

    Суперечність «кешувати довго, але бачити нове одразу» розвʼязує cache busting через зміну URL: змінився вміст — змінилася адреса. Практично це фінгерпринтинг, хеш вмісту в імені файлу (app.a1b2c3d4.js). Нова версія стає новим URL, тож старий кеш перестає бути релевантним і інвалідувати його не треба. Ключ схеми — HTML: його віддають із no-cache, бо саме він приносить актуальні імена, а хешовані асети — з річним immutable. Побічна деталь: хеші в іменах файлів — благо, а хеші в іменах CSS-класів — ні; локатор на автогенерованому класі ламатиметься на кожному .

    Наша практика (не канон). Канону під це немає — далі досвід команд. Проміжні вузли CDN звуть edge-вузлами. Головна операційна проблема — інвалідація: віддали файл із s-maxage=86400, задеплоїли фікс, і вузли ще добу віддають стару версію; звідси механізми примусового скидання (purge). Діагностичне правило: тест на стенді стабільно бачить старий фронтенд, а локально свіжо — підозрюйте CDN, не свій код. Лікується це процесом (purge після , перевірка версії застосунку перед стартом — імʼя хешованого бандла в HTML тут готовий маркер), а не паузами: пауза перетворює проблему на плаваючий . Версія в query-рядку (app.js?v=20260708) менш надійна за хеш в імені файлу: деякі CDN ігнорують query-рядок у ключі кешу.

    Посередники: проксі, шлюз, тунель

    CDN — не єдине місце, де відповідь застрягає. HTTP описує три форми (intermediary), і той самий вузол може перемикатися між ролями залежно від запиту.

    • Проксі (proxy) — агент пересилання, якого обирає клієнт; це і є визначальна ознака, а не «стоїть з боку клієнта». Типове застосування — звести запити організації через спільного посередника заради безпекових сервісів або спільного кешу.
    • (gateway, він же reverse proxy) — посередник, який поводиться як origin-сервер для вихідного зʼєднання; його вживають, зокрема, щоб прискорити сервер кешуванням-прискорювачем і розділити навантаження між машинами.
    • (tunnel) — сліпий ретранслятор, який повідомлень не змінює; активний тунель не вважається учасником HTTP-обміну.

    Окремий клас — вузли, що фільтрують трафік без відома відправника. Такий interception proxy відрізняється від звичайного проксі тим, що його не обирає клієнт; протокольно він «indistinguishable (at a protocol level) from an on-path attacker». Висновок для тестів: у корпоративній мережі чи на публічному Wi-Fi поведінка запиту може змінитися без жодної зміни в застосунку.

    Бонус для звітів: 502 і 504 віддає сервер, що діє «as a gateway or proxy» — зламалася ланка за посередником; 503 означає, що сам сервер тимчасово не здатен обробити запит.

    Service Worker: кеш, який живе в коді сторінки

    Service Worker — це JavaScript, який браузер виконує у фоні, окремо від сторінки, у власному потоці без доступу до DOM. Він працює як проксі між вебзастосунком, браузером і мережею, перехоплює навігаційні та ресурсні запити й доступний лише в захищеному контексті (HTTPS, з винятком http://localhost).

    Ключове для тестів: у воркера власне сховище — Cache API, не повʼязане з HTTP-кешем браузера, доступне не лише воркеру («The Cache API is available from pages and service workers») і спільне на весь origin. Відповідь із нього віддається через FetchEvent.respondWith() — і лише там, де це прописано в обробнику: «the service worker only uses the cache when you tell it when and how».

    Підступність додає життєвий цикл — Download, Install, Activate; встановлення запускається, лише коли файл трапився вперше або побайтово відрізняється від наявного; на install наповнюють кеш, на activate прибирають старі. Якщо активний воркер уже є, нова версія встановлюється у фоні, але не активується — це стан «worker in waiting»: контроль вона бере лише після закриття всіх вкладок сайту, якщо код не викликав skipWaiting(). Тому після деплою тест може працювати зі старою логікою кешування.

    Патерн джерелаЖаргонЦіна
    Cache, falling back to networkcache-firstресурс із кешу мережу не питає — звідси «залипання» після деплою
    Network falling back to cachenetwork-firstна повільному звʼязку користувач чекає, поки мережа впаде
    Stale-while-revalidateвіддає старе зараз, оновлення підтягує на наступний раз

    Наша практика (не канон). Прямого твердження про взаємодію no-store і Cache API джерела не дають — це наш вивід із того, що сховища окремі: коректний no-store на бекенді воркера не стримає. Звідси й порядок підозр: якщо тест бачить примару старої версії, а HTTP-заголовки правильні, перша підозра — Service Worker.

    Що видно у вкладці Network: позначки походження відповіді

    Рівень, з якого прийшла відповідь, видно в мережевій панелі інструментів розробника (DevTools). Колонка Size показує заголовки плюс тіло, як їх віддав сервер, — але для відповідей із кешу вона показує не байти, а .

    (from memory cache) чи (from disk cache) означає, що мережею не передавалося нічого: свіжість не минула, сервера навіть не питали. 304 Not Modified — навпаки, живий запит, у якому сервер підтвердив копію й не передав тіло. Два різні діагнози: у першому випадку зміна на сервері до вас не доїде, доки не мине свіжість, у другому ревалідація працює.

    Поруч живе Disable cache, що відтворює досвід відвідувача, який заходить уперше: «This more accurately emulates a first-time user's experience». Межу його дії дока обумовлює панеллю, а не станом вікна: щоб вимкнути кеш під час роботи в інших панелях, беруть панель Network conditions. Детальний розбір панелі — глава «DevTools: вкладка Network і дебаг».

    Наша практика (не канон). Самі позначки (from memory cache) і (from disk cache) у довіднику DevTools не описані — їхнє прочитання ми даємо з практики.

    Перехоплення мережі як спосіб обійти кеш у тесті

    Найнадійніший спосіб зробити тест незалежним від кешу — не вгадувати, що лежить у сховищах, а вирішувати за них. (network interception) — тема глави «Перехоплення й мокання мережі»; тут беремо з неї рівно те, що стосується кешу. Механізм дає тесту API для спостереження й зміни трафіку: під контроль потрапляють усі запити сторінки, зокрема XHR і fetch.

    Обробник маршруту ухвалює одне з трьох рішень: пропустити (запит іде на сервер, дорогою можна змінити URL, метод, заголовки чи тіло), підмінити (сервер не викликається, браузер отримує рукотворну відповідь) або обірвати. Перехоплення вибіркове — запити поза шаблоном ідуть як завжди; маршрут на рівні контексту діє й на попапи.

    // відповідь приходить із тесту, а не з кешу й не з мережі
    await context.route('**/api/config', (route) =>
      route.fulfill({ status: 200, body: JSON.stringify({ feature: 'on' }) }),
    );

    Дві пастки, які дока називає прямо. Заборонені заголовки (Cookie, Host, Content-Length та інші) підмінити не вийде — «If an override is provided for a forbidden header, it will be ignored»: тест, який «підмінив» Cookie, зеленіє на непідміненому запиті. І коли на один шаблон зареєстровано кілька обробників, спрацьовують вони у зворотному до реєстрації порядку, бо «the last registered route can always override all the previous ones»; передати керування далі можна через route.fallback(), тоді як route.continue() ланцюг зупиняє. Альтернатива ручній підміні — режим запису-відтворення через HAR: дані реалістичні, а від сервера тест уже не залежить.

    Наша практика (не канон). Порядок «спершу маршрут, потім дія» джерело показує в прикладі, але як припис не формулює — формулюємо ми: правило діє лише на запити після реєстрації. Наш і другий висновок: залишений маршрут перехоплюватиме запити наступних тестів у тому самому воркері, тож його — частина ізоляції.

    Практичний наслідок: свіжий контекст на прогін

    Найдешевша ізоляція від кешу — свіжий профіль браузера на прогін; у Playwright роль профілю грає окремий browser context. Це прибирає накопичений стан без спеціального коду.

    Service Worker знімають окремо, бо він живе поза HTTP-кешем. Нюанс, який рятує години: специфікація застерігає, що unregister() «only affects subsequent navigations» — у тому самому контексті воркер після виклику ще працює. Радикальніший варіант — опція контексту, яка не дає воркерам реєструватися:

    const context = await browser.newContext({ serviceWorkers: 'block' });

    І застереження, без якого попереднє стає карго-культом: вимикати кеш у всіх тестах підряд не можна. Якщо предмет перевірки — сама поведінка кешування (що на умовний запит справді повертається 304, що офлайн працює), кеш має бути ввімкнений: інакше ви тестуєте не той застосунок, який поїде в прод.

    Наша практика (не канон). Далі — наш висновок із того, що рівнів кешу кілька; стандарту під це немає. «Стара версія» приходить із чотирьох місць: HTTP-кеш браузера, Cache API воркера, CDN або проміжний проксі та кеш усередині застосунку (стан у памʼяті, сховище браузера, кеш HTTP-клієнта). Саме тому «почистив кеш» не є відтворюваною процедурою: фраза не називає ні рівня, ні інструмента, ні моменту, а два місця з чотирьох ручному очищенню в браузері не піддаються — воркер знімається окремо і не одразу, а спільний кеш живе на чужій машині. Відтворювана процедура звучить інакше: свіжий контекст на прогін, воркер заблоковано, перехоплення там, де потрібен детермінізм, версія застосунку перевірена перед стартом.

    Типові помилки

    • Виглядає як «no-cache вимикає кеш», а насправді копія зберігається — її просто валідують перед кожним використанням. Зберігання вимикає no-store, ціною повного тіла щоразу.
    • Виглядає як «поставили no-store — дані захищені», а насправді RFC називає директиву ненадійним і недостатнім механізмом приватності.
    • Виглядає як «public дозволяє кешувати спільним кешам», а насправді вона знімає заборону для будь-якого кешу; спільними керує private.
    • Виглядає як баг застосунку («кнопки немає»), а насправді завантажився старий бандл: (from disk cache) і 304 — два різні діагнози.
    • Виглядає як «заголовки правильні, отже кеш не винен», а насправді Service Worker віддає копію з Cache API, на яку HTTP-заголовки не впливають.
    • Виглядає як «Age показує, скільки відповідь лежить у CDN», а насправді це час від її генерації або ревалідації на origin-сервері.

    Підсумок

    1. Правила кешу диктує сервер заголовками відповіді — тому кеш є частиною контракту, яку перевіряють асертами.
    2. Свіжа копія віддається без мережі; застаріла проходить умовний запит із валідатором, і 304 означає «бери своє, тіло не шлю».
    3. Кешів кілька й вони різні за господарем: приватний браузерний, спільний у посереднику та CDN, програмований Cache API воркера — тому діагностика починається з рівня.
    4. private розмежовує спільний і приватний кеш, public лише знімає заборону зберігати, no-store не є механізмом приватності.
    5. Ізоляція від кешу — свіжий контекст плюс явне поводження з воркером, а не ритуал «почистив кеш»; але там, де предметом тесту є сам кеш, вимикати його не можна.

    Можливі питання

    • «Чим no-cache відрізняється від no-store Перевіряють, чи ви не читаєте директиву за назвою: сильна відповідь називає різницю у зберіганні й додає ціну кожного варіанта.
    • «Що таке 304 і хто його ініціює?» Дивляться, чи розумієте, що це відповідь на умовний запит, а не помилка, і що перевірку ініціює кеш, надсилаючи валідатор.
    • «Що змінює CDN у тестуванні?» Питання про межу відповідальності. Очікують: CDN — спільний кеш, він слухає private і s-maxage, і це рівень, який тестовий код не може ні почистити, ні вимкнути, — його обходять перехопленням або процесом.
    • «Тест бачить стару версію. Ваші дії?» Міні-кейс, у якому слухають порядок: спершу рівень (що каже панель — кеш, 304 чи живий запит), потім воркер, потім CDN, і лише тоді код.
    • «Як ізолювати тести від кешу?» Перевіряють, чи не почнете з паузи. Очікують свіжий контекст, свідоме поводження з воркером, перехоплення там, де потрібен детермінізм, — і застереження, що тести самого кешування вимикати кеш не можуть.

    Джерела

    Хто кешує і на якій підставі

    • RFC 9111 — HTTP Caching — означення кешу, поділ на приватні й спільні, правила зберігання та відповідей.
    • MDN — HTTP caching — керовані кеші (reverse proxy, CDN, Service Worker) і евристичне кешування без Cache-Control.

    Життєвий цикл запиту: у якій точці кеш перехоплює

    Заголовки, якими сервер керує кешем

    • RFC 9111 — HTTP Caching — директиви Cache-Control, порядок обчислення терміну свіжості, правило найсуворішої директиви й застереження про no-store.
    • MDN — HTTP caching — еквівалентність max-age=0, must-revalidate і no-cache разом із застереженням так не писати.

    Свіжість проти валідації: умовний запит і 304

    Спільний кеш проти приватного: private, public, s-maxage

    • RFC 9111 — HTTP Cachingprivate у простій і кваліфікованій формі, точне значення public, s-maxage і Age.
    • MDN — HTTP cachingVary як частина ключа кешу.

    CDN як спільний кеш і що це змінює для тестування

    • MDN Glossary — CDN — CDN як мережа проміжних серверів ближче до користувача й розвантаження origin.
    • RFC 9111 — HTTP Caching — означення спільного кешу й правила, за якими живе будь-який проміжний вузол (самого терміна CDN у документі немає).
    • MDN — HTTP caching — cache busting через зміну URL, фінгерпринтинг і пара «HTML з no-cache плюс асети з immutable».
    • Playwright — Locators — чому локатор не будують на CSS, який змінюється разом із версткою.

    Посередники: проксі, шлюз, тунель

    • RFC 9110 — HTTP Semantics — три форми посередника та їхні визначальні ознаки, спільний кеш як мотив проксі, interception proxy, різниця 502/504 і 503.

    Service Worker: кеш, який живе в коді сторінки

    • MDN — Service Worker API — воркер як проксі, окремий потік, захищений контекст, життєвий цикл, стан очікування і skipWaiting(), FetchEvent.respondWith().
    • web.dev — The Offline Cookbook — Cache API як окреме сховище, спільне на весь origin, і канонічні назви трьох патернів разом з їхньою ціною.

    Що видно у вкладці Network: позначки походження відповіді

    Перехоплення мережі як спосіб обійти кеш у тесті

    • Playwright — Network — спостереження й зміна трафіку, три рішення обробника, вибірковість перехоплення, дія контекстного маршруту, режим запису-відтворення через HAR.
    • Playwright API — class: Route — ігнорування заборонених заголовків, зворотний порядок обробників, різниця route.fallback() і route.continue().
    • Playwright API — class: Page — реєстрація маршруту й прибирання його після тесту.

    Практичний наслідок: свіжий контекст на прогін

    Пояснення

    «Поясни» працює з власним API-ключем Anthropic: запит іде з вашого браузера прямо до Anthropic.

    Свій API-ключ (BYOK)

    Вставте власний ключ Anthropic — пояснення працюватиме на реальній моделі. Ключ зберігається лише у вашому браузері (localStorage), нікуди не надсилається, крім api.anthropic.com, і не логується.