Кешування
Зміст
Кеш (cache) — це проміжне сховище, у якому браузер, проксі чи сервер тримають уже отримані відповіді, щоб наступного разу не тягнути їх мережею повторно. Ідея проста й стара, як сам веб: якщо файл app.js не змінився з учора, немає сенсу качати ті самі 300 кілобайтів удруге — розумніше взяти їх з диска за мілісекунди.
Для AQA кешування — це не абстрактна оптимізація продуктивності, а один із найпідступніших джерел флаку (flakiness) і фальшивих результатів. Тест «бачить» стару версію сторінки, локатор (locator) не знаходить нову кнопку, автотест зелений на застарілому бандлі — і ти годину шукаєш баг там, де його немає. Тому механіку кешу треба розуміти не гірше, ніж власне застосунок.
Що таке кеш браузера
Коли браузер завантажує сторінку, він робить десятки запитів: HTML, CSS, JavaScript, шрифти, картинки, дані з API. Кожну відповідь потенційно можна зберегти локально й перевикористати. Саме цим займається HTTP-кеш браузера (HTTP cache) — сховище, яке живе між мережею та рушієм рендерингу.
Технічно браузер тримає кілька рівнів:
| Рівень | Де живе | Термін життя | Що туди потрапляє |
|---|---|---|---|
| Memory cache | Оперативна памʼять | Поки вкладка відкрита | Ресурси поточної сесії, найшвидший доступ |
| Disk cache | Диск | Переживає перезапуск браузера | HTTP-відповіді за правилами кешування |
| Service Worker Cache | Диск (Cache API) | Керується кодом сторінки | Те, що явно поклав розробник |
Ключова думка: браузер сам не вирішує, що і як довго зберігати. Правила диктує сервер через заголовки відповіді (response headers). Це важливо для AQA: поведінка кешу — частина контракту між бекендом і фронтендом, її можна й потрібно тестувати. Сучасні правила HTTP-кешування описані в RFC 9111, який замінив старіший RFC 7234.
Розрізняють два принципові стани кешованої відповіді:
- Свіжа (fresh) — термін придатності ще не минув, браузер бере копію з кешу без жодного мережевого запиту.
- Застаріла (stale) — термін минув, і перш ніж використати відповідь, браузер має перепитати сервер. Це називають ревалідацією (revalidation).
Уся подальша тема — про те, як сервер керує цими двома станами.
Заголовок Cache-Control
Cache-Control — головний інструмент керування кешем у сучасному HTTP. Це заголовок, який сервер (а іноді й клієнт) надсилає з набором директив через кому.
HTTP/1.1 200 OK
Content-Type: application/javascript
Cache-Control: public, max-age=3600
Найважливіші директиви:
| Директива | Значення |
|---|---|
max-age=<секунди> | Скільки секунд відповідь вважається свіжою від моменту отримання |
no-cache | Зберігати можна, але перед кожним використанням треба ревалідувати на сервері |
no-store | Не зберігати взагалі нічого |
public | Дозволено кешувати спільним кешам (CDN, проксі) |
private | Кешувати може лише браузер користувача, спільним кешам — заборонено |
must-revalidate | Коли відповідь застаріла, використати її без перевірки не можна |
s-maxage=<секунди> | Як max-age, але лише для спільних кешів; має пріоритет над max-age для них |
immutable | Файл не зміниться за час свіжості, тож не ревалідувати навіть при перезавантаженні |
Директива max-age задає відносний час у секундах, на відміну від старішого заголовка Expires, який задає абсолютну дату. Коли присутні обидва, Cache-Control: max-age перемагає Expires.
Приклад типової пари правил на реальному сайті:
# HTML-документ: хочемо завжди свіжий, але дозволяємо швидку перевірку
Cache-Control: no-cache
# Захешований бандл: не змінюється ніколи, кешуємо на рік
Cache-Control: public, max-age=31536000, immutable
(31536000 секунд — це рівно 365 днів.)
no-cache проти no-store
Це місце, де плутаються навіть досвідчені інженери, бо назви оманливі. no-cache не означає «не кешувати».
no-store— буквально: не клади цю відповідь ні в памʼять, ні на диск. Кожен запит іде мережею з нуля. Так позначають чутливі дані: сторінки з банківськими виписками, відповіді з персональними даними, одноразові токени.no-cache— зберігати можна, але перед тим, як віддати збережену копію, браузер зобовʼязаний спитати сервер: «ця версія ще актуальна?» Якщо сервер відповідає304 Not Modified— беремо з кешу; якщо200з новим тілом — оновлюємо. Тобтоno-cacheекономить трафік (тіло не переказується, якщо нічого не змінилось), але не економить сам мережевий похід. За змістомno-cacheприблизно відповідаєmax-age=0, must-revalidate.
| Аспект | no-cache | no-store |
|---|---|---|
| Копія лягає в кеш | Так | Ні |
| Мережевий запит щоразу | Так (ревалідація) | Так (повне завантаження) |
| Тіло переказується щоразу | Ні, якщо 304 | Так, завжди |
| Типове застосування | HTML, який має бути свіжим | Чутливі/приватні дані |
Практична порада для AQA: якщо у вашому тесті сторінка щоразу тягнеться повністю й тест повільний — гляньте в мережеву панель, чи не стоїть на всьому no-store. А якщо, навпаки, тест ловить стару версію — можливо, ресурс віддається зі щедрим max-age без валідатора.
Валідація: умовні запити
Ревалідація працює через умовні запити (conditional requests). Браузер надсилає серверу «відбиток» тієї версії, що вже лежить у кеші, а сервер вирішує, чи вона ще актуальна. Є два механізми: за вмістом (ETag) і за датою (Last-Modified).
ETag і 304
ETag (entity tag) — це непрозорий ідентифікатор конкретної версії ресурсу, який сервер віддає у відповіді. Часто це хеш вмісту, але сервер може рахувати його й з метаданих файлу — розміру та часу зміни (саме так за замовчуванням роблять Apache й nginx). Клієнту байдуже, як саме, — головне, що при зміні файлу ETag змінюється.
Перший запит і відповідь:
GET /app.js HTTP/1.1
Host: example.com
HTTP/1.1 200 OK
Content-Type: application/javascript
Cache-Control: max-age=3600
ETag: "33a64df5c1e9"
(тіло файлу)
Коли свіжість минула, браузер не качає файл наосліп, а надсилає збережений ETag у заголовку If-None-Match:
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
304 Not Modified каже браузеру: «бери своє з кешу, воно ще годиться». Тіло не переказується — економимо трафік, але оновлюємо термін свіжості. Якщо ж файл змінився, сервер повертає звичайний 200 з новим тілом і новим ETag.
Розрізняють сильні (strong) й слабкі (weak) ETag. Слабкий позначають префіксом W/ (наприклад, W/"33a64df5") — він гарантує лише семантичну еквівалентність, а не побайтову ідентичність, тож не годиться для докачувань частинами (range requests).
Last-Modified / If-Modified-Since
Старіший і простіший механізм — за датою останньої зміни. Сервер віддає заголовок Last-Modified:
HTTP/1.1 200 OK
Cache-Control: max-age=3600
Last-Modified: Tue, 01 Jul 2026 10:00:00 GMT
(тіло)
При ревалідації браузер надсилає цю дату в If-Modified-Since:
GET /app.js HTTP/1.1
Host: example.com
If-Modified-Since: Tue, 01 Jul 2026 10:00:00 GMT
Сервер повертає 304, якщо з того часу файл не змінювався, або 200 з новим вмістом, якщо змінювався.
На практиці браузер часто надсилає обидва валідатори одночасно (If-None-Match та If-Modified-Since), якщо в кеші є обидва заголовки. Коли присутній ETag, він має пріоритет: за специфікацією умовних запитів сервер, обчисливши If-None-Match, If-Modified-Since уже не враховує.
| ETag / If-None-Match | Last-Modified / If-Modified-Since | |
|---|---|---|
| Основа порівняння | Вміст (зазвичай хеш) | Дата зміни |
| Точність | До байта | До 1 секунди |
| Ловить зміну «туди й назад» | Так (хеш повернеться) | Ні, якщо дата не змінилась |
| Ідентичні файли на різних нодах | Хеші можуть різнитися | Дата стабільніша |
| Пріоритет при наявності обох | Вищий | Нижчий |
Секундна точність Last-Modified — реальна пастка: якщо файл змінюють двічі за одну секунду, друга зміна може «загубитися». ETag від цього застрахований, тому для критичних ресурсів надійніший саме він.
Кеш поза браузером: CDN
Кеш браузера — лише останній рубіж. Між користувачем і вашим сервером зазвичай стоїть CDN (Content Delivery Network) — мережа проміжних серверів (їх називають edge- або крайовими вузлами), розкиданих географічно ближче до користувачів. CDN кешує відповіді, і більшість запитів на статику до вашого сервера (origin) взагалі не доходить.
CDN — це спільний кеш (shared cache), тому він реагує на директиви, орієнтовані на спільні кеші:
publicдозволяє йому кешувати;privateзабороняє (це лише для браузера конкретного користувача);s-maxageзадає окремий термін свіжості саме для спільних кешів, який перекриваєmax-age.
Два заголовки, які варто знати AQA під час дебагу через мережеву панель:
Age— скільки секунд відповідь уже пролежала у спільному кеші (точніше — оцінка кешу, скільки часу минуло від моменту, коли origin згенерував чи ревалідував відповідь). ЯкщоAgeблизький доmax-age, копія скоро застаріє.Vary— визначає, за якими заголовками запиту рахується ключ кешу. Наприклад,Vary: Accept-Encodingозначає, що gzip- і brotli-версії кешуються окремо. Хибний або занадто широкийVary(як-отVary: User-Agent) роздуває кеш і знижує його ефективність.
Окремі провайдери додають власні діагностичні заголовки на кшталт X-Cache: HIT / MISS або CF-Cache-Status (Cloudflare). Точні назви й формат залежать від провайдера, тож перед дебагом варто звіритися з його документацією.
Головна операційна проблема CDN — інвалідація (invalidation). Якщо ви віддали файл з s-maxage=86400, а потім задеплоїли фікс, CDN може ще добу віддавати стару версію по всьому світу. Тому CDN мають механізми примусового скидання кешу (purge). Для AQA це прямий ризик: після деплою вашого білда edge-вузол у вашому регіоні може ще тримати попередню версію, і автотест на staging/prod легко зловить її.
Практичний висновок: коли тест на реальному середовищі стабільно бачить старий фронтенд, а локально все свіже — підозрюйте CDN, а не свій код.
Service Worker (оглядово)
Service Worker — це JavaScript, який браузер виконує у фоні, окремо від сторінки, і який може перехоплювати всі мережеві запити цієї сторінки. По суті це програмований проксі на боці клієнта. Він лежить в основі PWA (Progressive Web Apps) і офлайн-режиму.
Service worker має власне сховище — Cache API, яке ніяк не повʼязане з HTTP-кешем браузера. Це критично: навіть якщо ви правильно виставили no-store на бекенді, service worker може віддавати сторінці свою збережену копію, і HTTP-заголовки на це не вплинуть.
Мінімальний приклад стратегії «спершу кеш» (cache-first):
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request);
})
);
});
Тут будь-який запит спершу шукається в Cache API, і лише за відсутності копії йде в мережу. Поширені стратегії:
- cache-first — швидко, але легко «залипає» на старій версії;
- network-first — свіжо, але повільніше й без офлайну;
- stale-while-revalidate — віддати старе миттєво, а нове підтягнути у фоні.
Життєвий цикл service worker (install → activate, а вже потім — обробка подій fetch) додає ще один шар підступності: новий воркер за замовчуванням не активується одразу, а переходить у стан очікування (waiting) й бере контроль лише після закриття всіх вкладок сайту (якщо код явно не викликав skipWaiting()), тож користувач і тест можуть працювати зі старою логікою кешування навіть після деплою.
Для AQA висновок простий: service worker — окремий, найупертіший рівень кешу. Якщо тест бачить примару старої версії, а HTTP-заголовки правильні, перша підозра — саме service worker.
Чому тест бачить стару версію сторінки і як із цим боротися
Зберемо все докупи з погляду автотестів. «Стара версія» може прийти з чотирьох різних місць, і лікуються вони по-різному:
- HTTP-кеш браузера — ресурс віддано зі щедрим
max-age, свіжість ще не минула. - Service Worker Cache — воркер віддає свою копію в обхід мережі.
- CDN / проміжний проксі — edge-вузол тримає застарілу відповідь.
- Кеш всередині самого застосунку — стан у памʼяті,
localStorage, кеш GraphQL/HTTP-клієнта. Формально це не мережеве кешування, але симптом той самий: стара картина на екрані.
Чому це особливо болить у тестах:
- Локатор не знаходить елемент, бо завантажився старий бандл без нової фічі — виглядає як баг застосунку, хоча це кеш.
- Тест «іноді» падає: на одному прогоні браузер узяв свіже, на іншому — з кешу. Класичний флак.
- Тест зелений на застарілій версії й пропускає реальну регресію.
- Стан протікає між тестами через
localStorageчи service worker — порушується ізоляція.
Як з цим борються:
Ізоляція браузерного профілю. Найнадійніший спосіб — не давати кешу накопичуватися між тестами. У Playwright кожен browser context — це фактично чистий профіль зі своїм сховищем; новий контекст стартує без попереднього кешу й localStorage. У Selenium аналогічно допомагає свіжий профіль на кожну сесію. Це прибирає протікання стану між тестами майже даром.
Вимкнення HTTP-кешу на рівні протоколу. У Chromium через CDP-сесію є команда Network.setCacheDisabled, яка глушить HTTP-кеш на весь час сесії — аналог галочки «Disable cache» у DevTools. Для не-Chromium рушіїв підхід інший (свіжий профіль або перехоплення запитів).
// Playwright, Chromium
const client = await context.newCDPSession(page);
await client.send('Network.setCacheDisabled', { cacheDisabled: true });
Перехоплення мережі (network interception). Оскільки автотести й так часто мокають або спостерігають запити, перехоплення дає повний контроль: можна віддавати свій вміст, підмінювати заголовки або примусово ходити в мережу. У Playwright це page.route, у Selenium — через CDP. Побічний бонус — тест перестає залежати від реального кешу взагалі.
Прибирання service worker. Оскільки воркер живе поза HTTP-кешем, його треба знімати окремо — інакше він переживе навіть чистий контекст, якщо профіль спільний:
// Виконати в контексті сторінки перед тестом
navigator.serviceWorker.getRegistrations()
.then((regs) => regs.forEach((r) => r.unregister()));
Для Playwright є ще опція serviceWorkers: 'block' на рівні контексту, яка не дає воркерам реєструватися взагалі — часто це найчистіший варіант для тестів, яким PWA-логіка не потрібна. Варто памʼятати, що service worker підтримують лише браузери на Chromium, тож ця опція найпомітніша саме там.
Синхронізація з деплоєм. Проти CDN код у тесті безсилий — тут допомагає процес: очікування (wait), поки CDN віддасть новий білд, примусовий purge після деплою або перевірка версії застосунку перед стартом сюїта. Не варто маскувати проблему кешу штучними затримками — це перетворює її на плаваючий флак.
Важливе застереження: агресивно вимикати кеш у всіх тестах — не завжди правильно. Якщо ви тестуєте саме поведінку кешування (наприклад, що 304 справді повертається, або що PWA працює офлайн), кеш має бути ввімкнений — інакше ви тестуєте не той застосунок, який поїде в прод. Розділяйте функціональні тести кешу й тести, де кеш — лише перешкода.
Cache busting (версіонування ресурсів)
Ми хочемо суперечливого: щоб браузер кешував статику максимально довго (швидкість) і водночас миттєво бачив нову версію після деплою (свіжість). Розвʼязання цієї суперечності називають cache busting — «підрив кешу».
Головний прийом — фінгерпринтинг (fingerprinting): у назву файлу вшивають хеш його вмісту.
<script src="/assets/app.a1b2c3d4.js"></script>
<link rel="stylesheet" href="/assets/main.9f8e7d6c.css">
Логіка тут елегантна:
- Файл
app.a1b2c3d4.jsвіддається зCache-Control: public, max-age=31536000, immutable— кешуй хоч на рік, він ніколи не зміниться. - Коли код правиться, змінюється й хеш:
app.a1b2c3d4.js→app.e5f6a7b8.js. Для браузера це новий URL, тож старий кеш просто перестає бути релевантним — його ніхто не запитує. - Стару копію можна не інвалідовувати: вона мирно доживає в кеші, ні на що не впливаючи.
Ключ, який тримає всю схему, — HTML-документ. Його не можна кешувати надовго, бо саме він містить посилання на актуальні хешовані файли. Тому HTML віддають з no-cache (або коротким max-age), а хешовані ассети — з річним immutable. HTML щоразу ревалідується й приносить свіжі імена бандлів; браузер бачить нові URL і докачує лише те, що реально змінилося.
Альтернатива — версія в query-рядку:
<script src="/assets/app.js?v=20260708"></script>
Це працює й простіше в реалізації, але має нюанс: деякі CDN за замовчуванням ігнорують query-рядок у ключі кешу або кешують такі URL інакше, тож надійність залежить від конфігурації. Фінгерпринтинг у назві файлу від цього вільний, тому вважається надійнішим підходом за замовчуванням.
Сучасні збирачі (Vite, webpack, esbuild) роблять фінгерпринтинг автоматично: рахують content hash і переписують посилання в HTML під час білда. Розробнику зазвичай нічого не треба робити руками.
Чим це важливо для AQA:
- Оракул свіжості. Хеш або версію в назві бандла можна використати як маркер: якщо після деплою в HTML усе ще старе імʼя файлу — розгортання не доїхало (або застряг CDN), і тест не має сенсу. Це швидка перевірка «а ми взагалі на правильній версії?».
- Стабільність локаторів. Хеші в назвах файлів — норма й благо. А от хеші в CSS-класах чи
id(наприклад,.button_a1b2c3від CSS-модулів) — пастка: локатор, привʼязаний до такого класу, ламатиметься на кожному білді. Прив’язуйтеся до стабільних атрибутів (data-testid, ARIA-ролі), а не до згенерованих імен. - Пояснення флаку. Розуміння схеми «HTML — no-cache, ассети — immutable» одразу підказує, де шукати, коли тест бачить примару: якщо HTML свіжий, а фіча стара — майже напевно винен service worker або CDN, бо самі хешовані ассети «залипнути» на неправильному вмісті не можуть за побудовою.
Кешування — це компроміс між швидкістю й свіжістю, розписаний у HTTP-заголовках. Хто читає ці заголовки в мережевій панелі й розуміє, з якого саме рівня прийшла відповідь, той відрізняє справжній баг застосунку від застарілої копії — і не витрачає години на полювання за примарою.
Що таке кеш і навіщо він потрібен вебу?
Кеш — це проміжне сховище, у якому браузер, проксі чи сервер тримають уже отримані відповіді, щоб не тягнути їх мережею повторно. Якщо файл app.js не змінився з учора, немає сенсу качати ті самі 300 кілобайтів удруге — швидше взяти їх з диска за мілісекунди. Кешувати можна майже все, що їде по HTTP: HTML, CSS, JavaScript, шрифти, картинки, відповіді API. Для користувача це швидкість, для інфраструктури — менше навантаження на сервер. Для тестувальника кеш — ще й одне з найпідступніших джерел флаку: тест може бачити стару версію сторінки і падати там, де бага немає.
Які рівні кешу є в браузері?
Браузер тримає щонайменше три рівні. Memory cache живе в оперативній памʼяті й діє, поки відкрита вкладка, — це найшвидший доступ до ресурсів поточної сесії. Disk cache лежить на диску, переживає перезапуск браузера і наповнюється HTTP-відповідями за правилами кешування. Service Worker Cache — окреме сховище (Cache API), яким керує код сторінки: туди потрапляє лише те, що явно поклав розробник. Ключовий момент: правила для HTTP-кешу диктує сервер через заголовки відповіді, тобто поведінка кешу — частина контракту між бекендом і фронтендом, і її можна тестувати.
Що означає, що кешована відповідь «свіжа» або «застаріла»?
Це два принципові стани копії в кеші. Свіжа (fresh) — термін придатності ще не минув, і браузер віддає копію з кешу без жодного мережевого запиту. Застаріла (stale) — термін минув, і перш ніж використати копію, браузер має перепитати сервер, чи вона ще актуальна; це називається ревалідацією (revalidation). Термін свіжості задає сервер, найчастіше через Cache-Control: max-age. Якщо сервер на ревалідацію відповідає 304 Not Modified — копія годиться; якщо 200 з новим тілом — кеш оновлюється.
Що робить заголовок Cache-Control і які його головні директиви?
Cache-Control — головний інструмент керування кешем у сучасному HTTP: сервер надсилає в ньому набір директив через кому. Основні: max-age задає, скільки секунд відповідь вважається свіжою; no-cache дозволяє зберігати, але вимагає ревалідації перед кожним використанням; no-store забороняє зберігати взагалі; public дозволяє кешувати спільним кешам (CDN, проксі), а private — лише браузеру користувача; must-revalidate забороняє використовувати застарілу копію без перевірки; immutable каже не ревалідувати файл навіть при перезавантаженні. На відміну від старішого Expires з абсолютною датою, max-age — відносний час у секундах, і коли присутні обидва, перемагає Cache-Control: max-age. Сучасні правила описані в RFC 9111.
У чому різниця між no-cache і no-store?
Назви оманливі: no-cache не означає «не кешувати». no-store — буквально не зберігати відповідь ні в памʼяті, ні на диску: кожен запит іде мережею з нуля, тіло переказується щоразу; так позначають чутливі дані — банківські виписки, персональні дані, одноразові токени. no-cache — зберігати можна, але перед видачею копії браузер зобовʼязаний спитати сервер, чи версія актуальна: на 304 бере з кешу, на 200 оновлює. Тобто no-cache економить трафік (тіло не їде, якщо нічого не змінилось), але не економить сам мережевий похід; за змістом це приблизно max-age=0, must-revalidate. Практичний наслідок для AQA: якщо тест повільний і все тягнеться повністю — можливо, на всьому стоїть no-store; якщо тест ловить стару версію — можливо, щедрий max-age без валідатора.
Що таке статус 304 Not Modified і коли сервер його повертає?
304 Not Modified — відповідь сервера на умовний запит, яка каже браузеру: «бери свою копію з кешу, вона ще годиться». Виникає так: коли свіжість минула, браузер не качає ресурс наосліп, а надсилає «відбиток» збереженої версії — ETag у заголовку If-None-Match або дату в If-Modified-Since. Якщо на сервері вміст той самий, він відповідає 304 без тіла — трафік економиться, а термін свіжості копії оновлюється. Якщо вміст змінився — повертається звичайний 200 з новим тілом і новим валідатором. В автотестах наявність 304 у мережевій панелі — нормальна ознака робочої ревалідації, а не помилка; більше того, якщо ви тестуєте саме кешування, повернення 304 — це те, що варто перевіряти явно.
Що таке ETag і як працює ревалідація через нього?
ETag (entity tag) — непрозорий ідентифікатор конкретної версії ресурсу, який сервер віддає у відповіді. Часто це хеш вмісту, але сервер може рахувати його й з метаданих файлу — розміру та часу зміни (так за замовчуванням роблять Apache і nginx); клієнту байдуже як, головне — при зміні файлу ETag змінюється. Механіка: перший запит приносить 200 з ETag: "33a64df5c1e9"; коли свіжість минула, браузер надсилає цей ETag у заголовку If-None-Match; збіг — сервер повертає 304 без тіла, розбіжність — 200 з новим вмістом. Є ще розподіл на сильні та слабкі ETag: слабкий позначається префіксом W/ і гарантує лише семантичну еквівалентність, а не побайтову ідентичність, тож не годиться для докачувань частинами (range requests).
Чим ETag відрізняється від Last-Modified і що має пріоритет?
Це два валідатори для умовних запитів: ETag порівнює за вмістом (зазвичай хеш), Last-Modified — за датою останньої зміни, яку браузер повертає в If-Modified-Since. Точність Last-Modified — одна секунда, і це реальна пастка: якщо файл змінили двічі за секунду, друга зміна може «загубитися». ETag ловить і зміну «туди й назад» (хеш повернеться до старого значення), а дата — ні, якщо не змінилась. Зворотний бік: на різних нодах ідентичні файли можуть мати різні ETag-и (якщо ті рахуються з метаданих), тоді як дата стабільніша. Браузер часто надсилає обидва валідатори одночасно, і за специфікацією пріоритет має ETag: обчисливши If-None-Match, сервер If-Modified-Since уже не враховує. Тому для критичних ресурсів надійніший саме ETag.
Що означають директиви public і private та навіщо існує s-maxage?
Ці директиви розмежовують браузерний і спільний (shared) кеш. public дозволяє кешувати відповідь спільним кешам — CDN і проксі; private забороняє це й лишає копію тільки в браузері конкретного користувача — так захищають персоналізовані відповіді від потрапляння в спільний кеш. s-maxage — це окремий термін свіжості саме для спільних кешів: для них він має пріоритет над max-age, тож можна тримати різні терміни для CDN і для браузера. Практичний наслідок: якщо відповідь з приватними даними випадково піде з public, її може закешувати CDN і віддати іншим користувачам — це вже не флак, а інцидент безпеки, і такі заголовки варто перевіряти в тестах API.
Що робить заголовок Vary?
Vary визначає, за якими заголовками запиту рахується ключ кешу, тобто скільки окремих копій ресурсу зберігатиметься. Наприклад, Vary: Accept-Encoding означає, що gzip- і brotli-версії кешуються окремо — інакше клієнт міг би отримати стиснуту версію, яку не вміє розпакувати. Хибний або занадто широкий Vary (класика — Vary: User-Agent) роздуває кеш на купу майже однакових копій і різко знижує його ефективність. Для AQA це один із заголовків, які варто читати в мережевій панелі під час дебагу: він пояснює, чому «той самий» URL для різних клієнтів приходить з різних копій кешу.
Як працює кешування на CDN і що таке проблема інвалідації?
CDN (Content Delivery Network) — мережа проміжних edge-вузлів, розкиданих географічно ближче до користувачів; вона кешує відповіді, і більшість запитів на статику до origin-сервера взагалі не доходить. CDN — спільний кеш, тому слухається public, private і s-maxage. Головна операційна проблема — інвалідація: якщо файл віддано з s-maxage=86400, а потім задеплоєно фікс, CDN може ще добу віддавати стару версію по всьому світу, тому провайдери мають механізми примусового скидання (purge). Для AQA це прямий ризик: після деплою edge-вузол у вашому регіоні може ще тримати попередній білд, і автотест на staging або prod зловить його. Практичне правило: якщо тест на реальному середовищі стабільно бачить старий фронтенд, а локально все свіже — підозрюйте CDN, а не свій код.
Якими заголовками можна зрозуміти, що відповідь прийшла з CDN-кешу?
Стандартний заголовок — Age: скільки секунд відповідь уже пролежала у спільному кеші (точніше, оцінка кешу, скільки минуло від моменту, коли origin згенерував чи ревалідував відповідь). Якщо Age близький до max-age, копія скоро застаріє. Додатково окремі провайдери ставлять власні діагностичні заголовки на кшталт X-Cache: HIT або MISS чи CF-Cache-Status у Cloudflare — точні назви залежать від провайдера, тож перед дебагом варто звіритися з його документацією. В автотестах такі заголовки зручно використовувати як діагностику: перехопивши відповідь, можна залогувати, з якого рівня вона прийшла, і не гадати, чому тест бачить стару версію.
Що таке cache busting і як працює фінгерпринтинг ресурсів?
Cache busting розвʼязує суперечність «кешувати статику максимально довго, але миттєво бачити нову версію після деплою». Головний прийом — фінгерпринтинг: у назву файлу вшивають хеш його вмісту, наприклад app.a1b2c3d4.js, і віддають з Cache-Control: public, max-age=31536000, immutable — кешуй хоч рік, файл ніколи не зміниться. Коли код правиться, змінюється хеш, отже й URL — для браузера це новий ресурс, і старий кеш просто ніхто більше не запитує, тож інвалідація не потрібна. Ключ схеми — HTML-документ: він містить посилання на актуальні хешовані файли, тому його віддають з no-cache (або коротким max-age), щоб щоразу ревалідувався. Альтернатива — версія в query-рядку (app.js?v=20260708), але деякі CDN ігнорують query-рядок у ключі кешу, тож хеш у назві файлу вважається надійнішим. Сучасні збирачі (Vite, webpack, esbuild) роблять це автоматично під час білда.
Що таке Service Worker і чому HTTP-заголовки на його кеш не впливають?
Service Worker — це JavaScript, який браузер виконує у фоні окремо від сторінки і який може перехоплювати всі її мережеві запити; по суті це програмований проксі на боці клієнта, основа PWA й офлайн-режиму. Він має власне сховище — Cache API, яке ніяк не повʼязане з HTTP-кешем браузера: навіть якщо бекенд правильно виставив no-store, воркер може віддати сторінці свою збережену копію, і заголовки на це не вплинуть. Типові стратегії: cache-first (швидко, але легко залипає на старому), network-first (свіжо, але повільніше й без офлайну), stale-while-revalidate (віддати старе миттєво, нове підтягнути у фоні). Додаткова підступність — життєвий цикл: новий воркер після деплою за замовчуванням висить у стані очікування і бере контроль лише після закриття всіх вкладок сайту, якщо код явно не викликав skipWaiting(). Для AQA висновок: якщо тест бачить примару старої версії, а HTTP-заголовки правильні, перша підозра — service worker.
Автотест бачить стару версію сторінки. Звідки вона могла взятися і як діагностувати?
Стара версія може прийти з чотирьох різних місць, і лікуються вони по-різному: HTTP-кеш браузера (щедрий max-age, свіжість не минула), Service Worker Cache (воркер віддає копію в обхід мережі), CDN чи проміжний проксі (edge-вузол тримає застарілу відповідь) і кеш усередині самого застосунку — стан у памʼяті, localStorage, кеш GraphQL-клієнта. Діагностику підказує схема cache busting: якщо HTML свіжий, а фіча стара — майже напевно винен service worker або CDN, бо хешовані ассети «залипнути» на неправильному вмісті не можуть за побудовою. Хеш у назві бандла взагалі зручно використовувати як оракул свіжості: якщо після деплою в HTML усе ще старе імʼя файлу — розгортання не доїхало або застряг CDN, і ганяти тести немає сенсу. Симптоми в тестах класичні: локатор не знаходить елемент зі старого бандла, тест «іноді» падає залежно від того, звідки прийшла копія, або зелений прогін пропускає реальну регресію.
Як ізолювати або вимкнути кеш в автотестах?
Найнадійніший базовий засіб — ізоляція браузерного профілю: у Playwright кожен browser context — фактично чистий профіль зі своїм сховищем, новий контекст стартує без кешу й localStorage; у Selenium аналогічно допомагає свіжий профіль на кожну сесію. Другий рівень — вимкнення HTTP-кешу на рівні протоколу: у Chromium через CDP-сесію є команда Network.setCacheDisabled — аналог галочки «Disable cache» у DevTools. Третій — перехоплення мережі (page.route у Playwright, CDP у Selenium): тест сам вирішує, що віддавати, і перестає залежати від кешу взагалі. Service worker треба знімати окремо, бо він живе поза HTTP-кешем: або unregister() для всіх реєстрацій перед тестом, або опція контексту serviceWorkers: 'block' у Playwright, яка не дає воркерам реєструватися взагалі. Проти CDN код у тесті безсилий — там допомагає процес: purge після деплою, очікування нового білда або перевірка версії застосунку перед стартом сюїта, але не штучні затримки, які лише перетворюють проблему на плаваючий флак.
Чи правильно вимикати кеш у всіх автотестах без винятку?
Ні, і це питання відрізняє механічне «завчив рецепт» від інженерного розуміння. Вимкнений кеш прибирає флак і протікання стану між тестами, тому для більшості функціональних UI-тестів це розумний дефолт — чистий контекст на тест коштує майже нічого. Але якщо ви тестуєте саме поведінку кешування — що сервер справді повертає 304 на умовний запит, що ассети віддаються з immutable, що PWA працює офлайн через service worker — кеш має бути ввімкнений, інакше ви тестуєте не той застосунок, який поїде в прод. Тому правильна відповідь — розділяти: окремо функціональні тести кешу з увімкненим кешем і окремо тести, де кеш — лише перешкода, з ізольованим профілем. Кешування — це компроміс між швидкістю й свіжістю, зафіксований у заголовках, і тестова стратегія має цей компроміс поважати, а не глушити навмання.
Три кейси, де кеш вирішує, зелений тест чи полювання за примарою: читання заголовків кешу в мережевій панелі DevTools (звідки саме прийшла відповідь), вимкнення HTTP-кешу й service worker у Playwright плюс перевірка версії бандла як оракула свіжості, і API-тест на контракт ревалідації ETag → 304. Скрізь — що дивитися і чому.
Кейс 1. DevTools Network: звідки насправді прийшла відповідь
Тест бачить стару кнопку, локатор не знаходить нову фічу. Перш ніж лізти в код застосунку, треба відповісти на одне питання: цю відповідь узяли з мережі чи з кешу — і з якого саме рівня. Відкрийте вкладку Network, увімкніть колонки та перезавантажте сторінку.
Колонка Size для захешованого бандла покаже не розмір у кілобайтах, а джерело:
Name Status Size Time
app.a1b2c3d4.js 200 (memory cache) 0 ms
main.9f8e7d6c.css 200 (disk cache) 1 ms
index.html 304 241 B 120 ms
Клацніть на конкретний запит і подивіться заголовки відповіді:
HTTP/1.1 200 OK
Cache-Control: public, max-age=31536000, immutable
ETag: "33a64df5c1e9"
Age: 84200
X-Cache: HIT
Що дивитися і чому:
(memory cache)/(disk cache)в Size — це200, але без мережі. Свіжість ще не минула (max-age), браузер узяв копію локально й сервера навіть не питав. Якщо тест ловить стару версію — саме звідси: ресурс віддано зі щедримmax-age, і чистогоreloadзамало.304 Not Modifiedнаindex.htmlозначає ревалідацію: браузер спитав сервер, той підтвердив, що копія годиться, тіло не переказувалося. Це нормальна роботаno-cacheна HTML — документ щоразу перевіряється й приносить свіжі імена хешованих ассетів.Ageпоказує, скільки секунд відповідь пролежала у спільному кеші (тут ~23 години — дрібниця проти річногоmax-ageimmutable-ассета; та колиAgeнаближається доmax-age, копія скоро застаріє). Разом ізX-Cache: HIT(чиCF-Cache-Statusу Cloudflare) це сигнал: відповідь прийшла з CDN, а не з origin. Коли на реальному середовищі стабільно стара версія, а локально свіжо — підозрюйте саме edge-вузол.- HTML свіжий, а фіча стара — окремий діагноз. Самі хешовані ассети «залипнути» на неправильному вмісті не можуть за побудовою (новий вміст = новий URL). Тож якщо
index.htmlуже з новим ім'ям бандла, а екран старий — винен service worker або CDN, не HTTP-кеш файлів.
Кейс 2. Playwright: вимкнути кеш, зняти service worker, звірити версію
Найпоширеніша причина флаку «іноді бачить стару збірку» — кеш, що протікає між прогонами. У Chromium HTTP-кеш глушать через CDP, service worker блокують на рівні контексту, а факт «ми взагалі на правильній версії» перевіряють маркером у бандлі.
import { test, expect, chromium } from '@playwright/test';
test.describe('без кешу і без service worker', () => {
test('локатор нової фічі не залежить від застарілої збірки', async () => {
const browser = await chromium.launch();
// serviceWorkers: 'block' — воркер не зареєструється взагалі
const context = await browser.newContext({ serviceWorkers: 'block' });
const page = await context.newPage();
// глушимо HTTP-кеш на весь час сесії (аналог Disable cache у DevTools)
const client = await context.newCDPSession(page);
await client.send('Network.setCacheDisabled', { cacheDisabled: true });
await page.goto('https://app.example.com/dashboard');
await expect(page.getByTestId('new-feature-button')).toBeVisible();
await context.close();
await browser.close();
});
});
Якщо профіль між тестами спільний, а serviceWorkers: 'block' не підходить (тестуєте не Chromium або сам застосунок реєструє воркер), його знімають руками просто в контексті сторінки:
await page.evaluate(async () => {
const regs = await navigator.serviceWorker.getRegistrations();
await Promise.all(regs.map((r) => r.unregister()));
});
Оракул свіжості — перевірити версію ще до першого асерту, щоб не гнати сюїт по застарілому фронтенді:
const html = await (await context.request.get('https://app.example.com/')).text();
// у HTML має бути ім'я бандла ПОТОЧНОГО деплою; старе ім'я = розгортання не доїхало
expect(html).toMatch(/app\.[a-f0-9]{8}\.js/);
Що дивитися і чому:
Network.setCacheDisabled— тільки Chromium. Для інших рушіїв підхід інший: свіжий профіль на сесію або перехоплення запитів черезpage.route. Новийbrowser contextі так стартує з чистим HTTP-кешем таlocalStorage, тож ізоляція контекстом прибирає протікання майже даром.- Service worker переживе навіть чистий контекст, якщо профіль спільний. Він живе поза HTTP-кешем у власному Cache API, тож
setCacheDisabledна нього не діє — знімати треба окремо черезunregister()абоserviceWorkers: 'block'. - Не вимикайте кеш у всіх тестах підряд. Якщо перевіряєте саме поведінку кешу (що
304повертається, що PWA працює офлайн) — кеш має бути ввімкнений, інакше тестуєте не той застосунок, що поїде в прод. - Проти CDN код у тесті безсилий. Тут рятує процес: очікування нового білда чи
purgeпісля деплою. Маскувати відставання CDN штучнимsleepне можна — це перетворює проблему на плаваючий флак.
Кейс 3. API-тест: контракт ревалідації ETag → 304
Поведінка кешу — частина контракту бекенду, її тестують без браузера. Перевіряємо, що сервер віддає ETag, а на умовний запит з If-None-Match відповідає 304 Not Modified без тіла.
import { test, expect } from '@playwright/test';
test('умовний запит: ETag дає 304 без тіла', async ({ request }) => {
// 1) перший запит — забираємо ETag
const first = await request.get('https://app.example.com/app.js');
expect(first.status()).toBe(200);
const etag = first.headers()['etag'];
expect(etag, 'сервер не віддав ETag — ревалідувати нема чим').toBeTruthy();
// 2) повторний запит із тим самим валідатором
const second = await request.get('https://app.example.com/app.js', {
headers: { 'If-None-Match': etag },
});
// сервер має підтвердити копію, а не слати тіло вдруге
expect(second.status()).toBe(304);
expect(await second.text()).toBe('');
expect(second.headers()['etag']).toBe(etag);
});
Можна перевірити й самі директиви кешування — що HTML не кешується надовго, а хешований ассет віддається з річним immutable:
test('HTML — no-cache, хешований бандл — immutable', async ({ request }) => {
const html = await request.get('https://app.example.com/');
expect(html.headers()['cache-control']).toContain('no-cache');
const asset = await request.get('https://app.example.com/assets/app.a1b2c3d4.js');
const cc = asset.headers()['cache-control'];
expect(cc).toContain('max-age=31536000');
expect(cc).toContain('immutable');
});
Що дивитися і чому:
304мусить приходити без тіла. У цьому весь сенсno-cache/ревалідації: мережевий похід є, але тіло не переказується. Якщо наIf-None-Matchсервер щоразу шле200з повним тілом — валідатор зламаний, і кеш працює вхолосту.- Немає
ETag— немає чим ревалідувати. Тоді браузер або тягне файл наосліп, або спирається наLast-Modifiedз його секундною точністю (дві зміни за секунду можуть «загубитися»). Для критичних ресурсів надійніший самеETag. no-store≠no-cache. Якщо на статиці стоїтьno-store, тіло їде щоразу повністю й тест повільний — це не про свіжість, а про заборону зберігати. Плутати їх у контракті небезпечно:no-storeдля бандлів уб'є продуктивність, аno-cacheна чутливих даних лишить їх у кеші.immutableна HTML — червоний прапор. Кешувати надовго можна лише хешовані ассети; сам HTML має ревалідуватися, бо саме він приносить актуальні імена бандлів. Ассерт на цю пару директив ловить неправильну конфігурацію кешу до того, як вона стане флаком у прод-тестах.
Кеш браузера і Cache-Control
- Розумію, що кеш вирішує компроміс «швидкість проти свіжості», а правила диктує сервер заголовками відповіді, а не браузер сам.
- Знаю три рівні браузерного кешу (memory, disk, Service Worker Cache) і чим вони різняться за терміном життя.
- Можу пояснити різницю між свіжою (fresh) і застарілою (stale) відповіддю та що ревалідація потрібна лише для stale.
- Знаю ключові директиви
Cache-Control:max-age,public/private,must-revalidate,s-maxage,immutable. - Можу пояснити різницю
no-cachevsno-store: перший кешує, але ревалідує; другий не зберігає взагалі — і чомуno-cacheце не «не кешувати».
Валідація й умовні запити
- Розумію, як працює ETag: сервер віддає відбиток версії, браузер шле його в
If-None-Match, у відповідь отримує304без тіла або200з новим вмістом. - Знаю пару
Last-Modified/If-Modified-Sinceі чому її секундна точність — пастка при двох змінах за секунду. - Можу пояснити, чому при наявності обох валідаторів ETag має пріоритет над датою.
- Розумію, чим
304 Not Modifiedекономить (тіло не переказується), а чим ні (мережевий похід усе одно є).
Кеш поза браузером: CDN
- Можу пояснити, що CDN — це спільний (shared) кеш, тож реагує на
public/privateіs-maxage, а не лише наmax-age. - Знаю діагностичні заголовки
AgeтаVaryі чому надто широкийVary(якUser-Agent) роздуває кеш. - Розумію проблему інвалідації CDN: після деплою edge-вузол може ще довго віддавати стару версію, і код у тесті проти цього безсилий.
Service Worker
- Розумію, що service worker — програмований проксі на клієнті з власним Cache API, не повʼязаним із HTTP-кешем, тож переб'є навіть
no-store. - Знаю стратегії cache-first / network-first / stale-while-revalidate і чим кожна ризикована.
- Можу пояснити, чому новий воркер не активується одразу (стан waiting без
skipWaiting()) і як це тримає стару логіку після деплою.
Чому тест бачить стару версію і cache busting
- Знаю чотири джерела «старої версії» (HTTP-кеш, Service Worker, CDN, кеш усередині застосунку) і що лікуються вони по-різному.
- Можу пояснити способи боротьби в автотестах: свіжий
browser context,Network.setCacheDisabledчерез CDP, перехоплення мережі, зняття service worker. - Розумію, чому не варто глушити кеш у всіх тестах: якщо перевіряю саме поведінку кешу чи офлайн — кеш має бути ввімкнений.
- Можу пояснити cache busting через фінгерпринтинг: хешоване імʼя файлу +
immutableна рік, а HTML зno-cacheприносить свіжі імена бандлів. - Розумію, чому хеші в назвах файлів — благо, а хеші в CSS-класах/
id— пастка для локаторів, і чому прив'язуватися треба доdata-testid/ARIA.
Хто вирішує, що і як довго браузер зберігає в HTTP-кеші?
Питання
Свіжа (fresh) і застаріла (stale) відповідь у кеші — у чому різниця для браузера?