Пошук уроків, статей та іншого контенту
Застосуєте кешування HTTP і Cache API, щоб зменшити кількість запитів та підтримати роботу застосунку офлайн.
Кешування зменшує:
кількість мережевих запитів;
час завантаження сторінки;
споживання трафіку;
навантаження на сервер;
залежність застосунку від стабільного інтернет-з’єднання.
У браузері можуть одночасно працювати кілька різних механізмів кешування:
HTTP-кеш браузера — керується заголовками HTTP і використовується самим браузером.
Cache API — програмний кеш, доступний через JavaScript, найчастіше у Service Worker.
Кеш даних застосунку — наприклад, IndexedDB для структурованих даних, які не є HTTP-відповідями.
Ці механізми не є взаємозамінними. HTTP-кеш добре підходить для звичайного повторного завантаження ресурсів, а Cache API дає застосунку контроль над стратегією кешування та підтримкою офлайн-режиму.
HTTP-кеш працює на основі заголовків відповіді сервера. Найважливіший із них — Cache-Control.
Cache-Control: max-ageCache-Control: public, max-age=3600Браузер може використовувати відповідь без повторного запиту протягом 3600 секунд.
Для ресурсів із версіюванням у назві, наприклад app.8f31c.js, часто використовують довгий час життя:
Cache-Control: public, max-age=31536000, immutableЦе безпечно, якщо після зміни вмісту змінюється і назва файлу.
no-cache та no-storeЦі директиви мають різне значення:
Cache-Control: no-cacheВідповідь можна зберегти, але перед повторним використанням її потрібно перевірити на сервері.
Cache-Control: no-storeВідповідь не слід зберігати в HTTP-кеші.
no-cache не означає «не кешувати». Він означає «завжди перевіряти актуальність перед використанням».
ETag і Last-ModifiedСервер може додати до відповіді ідентифікатор версії:
ETag: "users-v17"Під час наступного запиту браузер надсилає:
If-None-Match: "users-v17"Якщо ресурс не змінився, сервер відповідає:
HTTP/1.1 304 Not ModifiedТіло відповіді повторно не передається, тому економиться трафік.
Інший механізм використовує час зміни:
Last-Modified: Wed, 29 Jul 2026 10:00:00 GMTБраузер надсилає:
If-Modified-Since: Wed, 29 Jul 2026 10:00:00 GMTETag зазвичай надійніший, оскільки час зміни файлу не завжди достатньо точно описує його версію.
VaryЗаголовок Vary вказує, від яких заголовків запиту залежить відповідь:
Vary: Accept-EncodingДля різних значень Accept-Encoding браузер може зберігати різні варіанти відповіді.
Якщо сервер повертає різний контент залежно від Accept-Language, це також потрібно врахувати:
Vary: Accept-LanguageНеправильне налаштування Vary може призвести до видачі одному користувачу відповіді, згенерованої для іншого варіанта запиту.
fetchМетод fetch має параметр cache, який визначає взаємодію запиту з HTTP-кешем браузера.
const response = await fetch("/api/products", {
cache: "no-cache",
});Основні режими:
default — стандартна поведінка браузера;
no-store — не читати та не записувати HTTP-кеш;
reload — завантажити ресурс із мережі та оновити кеш;
no-cache — використати кешовану відповідь лише після перевірки її актуальності;
force-cache — спочатку використати кеш, навіть якщо відповідь застаріла;
only-if-cached — використати лише кешовану відповідь.
only-if-cached можна застосовувати лише для same-origin-запитів. Якщо відповідь відсутня в кеші, запит завершується помилкою.
Наприклад, для отримання актуальних даних із підтримкою ETag зазвичай достатньо:
const response = await fetch("/api/profile", {
cache: "no-cache",
});Браузер зможе використати попередню відповідь, але спочатку перевірить її на сервері.
Cache API — це асинхронне сховище пар Request і Response.
Відкрити кеш:
const cache = await caches.open("app-v1");Зберегти відповідь:
const response = await fetch("/styles.css");
if (response.ok) {
await cache.put("/styles.css", response);
}Отримати відповідь:
const cachedResponse = await cache.match("/styles.css");Видалити запис:
await cache.delete("/styles.css");Переглянути імена кешів:
const cacheNames = await caches.keys();
console.log(cacheNames);Важливо: Cache API не є просто іншим інтерфейсом до HTTP-кешу. Це окреме сховище, яким керує код застосунку.
Зокрема, Cache API:
не має автоматичної політики протермінування;
не видаляє старі записи самостійно;
не гарантує актуальність відповіді;
не застосовує до cache.put() усі правила Cache-Control;
потребує явної стратегії оновлення та очищення.
Service Worker — це спеціальний JavaScript-файл, який працює окремо від основного потоку сторінки та може перехоплювати мережеві запити.
Він може:
повертати відповідь із Cache API;
спочатку виконувати мережевий запит;
працювати офлайн;
кешувати ресурси під час встановлення;
видаляти старі версії кешу під час активації.
Service Worker працює лише в захищеному контексті:
через HTTPS;
або на localhost під час локальної розробки.
Нижче наведено приклад застосунку з такими стратегіями:
статичні ресурси — cache first;
API-запити — network first;
навігація сторінками — network first із резервним офлайн-ресурсом.
Код можна розмістити в основному JavaScript-файлі сторінки:
if ("serviceWorker" in navigator) {
window.addEventListener("load", async () => {
try {
const registration = await navigator.serviceWorker.register("/sw.js");
console.log("Service Worker зареєстровано:", registration.scope);
} catch (error) {
console.error("Не вдалося зареєструвати Service Worker:", error);
}
});
}Файл sw.js має бути доступний за адресою /sw.js. За замовчуванням Service Worker контролює сторінки у своїй директорії та її піддиректоріях.
sw.jsconst STATIC_CACHE = "static-v2";
const API_CACHE = "api-v2";
const STATIC_ASSETS = [
"/",
"/index.html",
"/app.js",
"/styles.css",
];
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(STATIC_CACHE).then((cache) => {
// Попередньо кешуємо критичні ресурси застосунку.
return cache.addAll(STATIC_ASSETS);
}),
);
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
const currentCaches = new Set([STATIC_CACHE, API_CACHE]);
return Promise.all(
cacheNames
.filter((cacheName) => !currentCaches.has(cacheName))
.map((cacheName) => caches.delete(cacheName)),
);
}),
);
});
self.addEventListener("fetch", (event) => {
const request = event.request;
const url = new URL(request.url);
// Не кешуємо запити, які можуть змінювати дані.
if (request.method !== "GET") {
return;
}
// Для зовнішніх ресурсів у цьому прикладі стратегія не застосовується.
if (url.origin !== self.location.origin) {
return;
}
if (url.pathname.startsWith("/api/")) {
event.respondWith(networkFirst(request));
return;
}
if (
request.destination === "document" ||
request.destination === "script" ||
request.destination === "style" ||
request.destination === "image"
) {
event.respondWith(cacheFirst(request));
}
});
async function cacheFirst(request) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const cache = await caches.open(STATIC_CACHE);
// Копіюємо відповідь, оскільки тіло Response можна прочитати лише один раз.
await cache.put(request, networkResponse.clone());
}
return networkResponse;
}
async function networkFirst(request) {
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const cache = await caches.open(API_CACHE);
// Зберігаємо копію відповіді для наступного офлайн-запиту.
await cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Для переходу на сторінку повертаємо кешований shell застосунку.
if (request.destination === "document") {
const appShell = await caches.match("/index.html");
if (appShell) {
return appShell;
}
}
// Якщо немає ні мережі, ні кешу, повертаємо стандартну помилку.
return Response.error();
}
}У цьому прикладі версія кешу змінюється з v1 на v2 під час нового розгортання. Під час події activate старі кеші видаляються.
Алгоритм:
перевірити Cache API;
якщо відповідь знайдена — повернути її;
якщо відповіді немає — виконати мережевий запит;
зберегти успішну відповідь у кеш.
Переваги:
мінімальна затримка;
добре підходить для незмінних ресурсів;
працює офлайн після першого завантаження.
Недоліки:
користувач може довго бачити застарілу версію;
потрібне версіювання кешу або явне оновлення.
Типові ресурси:
хешовані JavaScript-файли;
CSS;
шрифти;
зображення;
статичний app shell.
Алгоритм:
виконати мережевий запит;
якщо він успішний — повернути відповідь і зберегти її;
якщо мережа недоступна — використати кеш.
Переваги:
за наявності мережі користувач отримує свіжі дані;
підтримується офлайн-режим.
Недоліки:
без мережі відповідь може чекати тайм-аут;
кеш може містити застарілі дані.
Типові ресурси:
профіль користувача;
список замовлень;
новини;
динамічні API-відповіді.
Алгоритм:
негайно повернути кешовану відповідь;
паралельно виконати мережевий запит;
оновити кеш новою відповіддю.
Ця стратегія забезпечує швидкий інтерфейс, але перший показ може містити застарілі дані.
Спрощена реалізація:
async function staleWhileRevalidate(request) {
const cache = await caches.open("api-v1");
const cachedResponse = await cache.match(request);
const networkResponsePromise = fetch(request)
.then(async (response) => {
if (response.ok) {
await cache.put(request, response.clone());
}
return response;
})
.catch(() => null);
if (cachedResponse) {
// Оновлення кешу відбувається паралельно з поверненням старої відповіді.
return cachedResponse;
}
const networkResponse = await networkResponsePromise;
if (networkResponse) {
return networkResponse;
}
return Response.error();
}У реальному Service Worker фонове оновлення слід додатково пов’язувати з event.waitUntil(), щоб браузер не завершив роботу обробника раніше:
self.addEventListener("fetch", (event) => {
if (new URL(event.request.url).pathname.startsWith("/api/")) {
event.respondWith(
(async () => {
const cache = await caches.open("api-v1");
const cachedResponse = await cache.match(event.request);
const updatePromise = fetch(event.request)
.then(async (response) => {
if (response.ok) {
await cache.put(event.request, response.clone());
}
return response;
})
.catch(() => null);
event.waitUntil(updatePromise);
if (cachedResponse) {
return cachedResponse;
}
const networkResponse = await updatePromise;
return networkResponse || Response.error();
})(),
);
}
});Request і ResponseОб’єкти Request і Response містять потокове тіло. Після використання потік може бути вичерпаний.
Тому цей код є неправильним:
const response = await fetch(request);
await cache.put(request, response);
return response;Після передачі response у деяких сценаріях тіло вже не можна повторно прочитати для повернення сторінці.
Правильний варіант:
const response = await fetch(request);
await cache.put(request, response.clone());
return response;Клон відповіді дає змогу одну копію зберегти в кеші, а іншу повернути браузеру.
Кешування API потребує більшої обережності, ніж кешування статичних файлів.
Перед кешуванням потрібно визначити:
чи є відповідь публічною;
чи залежить вона від користувача;
чи містить персональні дані;
скільки часу допустима застарілість;
як інвалідувати старі дані;
як обробляти створення, зміну та видалення ресурсів.
Не слід бездумно кешувати персоналізовані відповіді:
Cache-Control: private, no-cacheДля чутливих відповідей може бути потрібна заборона зберігання:
Cache-Control: no-storeЯкщо відповідь залежить від cookie або заголовка авторизації, кешування в спільному сховищі може створити ризик витоку даних. Особливо небезпечно кешувати відповідь за URL, ігноруючи значення заголовка Authorization.
Для мутацій API — POST, PUT, PATCH, DELETE — зазвичай не використовують просту стратегію кешування. Після успішної мутації можна:
видалити пов’язаний запис із Cache API;
оновити кеш новою відповіддю;
позначити список застарілим;
повторно завантажити актуальні дані.
Наприклад:
async function updateProfile(profile) {
const response = await fetch("/api/profile", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(profile),
});
if (!response.ok) {
throw new Error("Не вдалося оновити профіль");
}
const cache = await caches.open("api-v2");
// Після зміни даних стару відповідь більше не використовуємо.
await cache.delete("/api/profile");
return response.json();
}Cache API не знає, коли дані стали неактуальними. Потрібно самостійно обрати механізм інвалідації.
Найпростіший підхід — змінювати ім’я кешу після нового розгортання:
const STATIC_CACHE = "static-v3";Під час activate усі невідомі кеші видаляються.
Цей підхід добре працює для статичних ресурсів, але не вирішує проблему актуальності динамічних API-даних.
Cache API не зберігає автоматично час додавання запису. Якщо потрібен TTL, його можна зберігати окремо, наприклад у спеціальному заголовку або в IndexedDB.
Для простих випадків можна використовувати HTTP-заголовки сервера та мережеву стратегію, яка періодично перевіряє ресурс.
Після зміни пов’язаних даних застосунок може видалити конкретний кешований запит:
const cache = await caches.open("api-v2");
await cache.delete("/api/products");
await cache.delete("/api/products?category=books");Потрібно враховувати, що різні query-параметри є різними ключами кешу.
За замовчуванням такі URL є різними:
/api/products?page=1
/api/products?page=1&sort=priceЯкщо застосунку потрібно вважати їх еквівалентними, ключ необхідно нормалізувати вручну. Робити це можна лише тоді, коли порядок або набір параметрів справді не впливає на відповідь.
Не можна бездумно видаляти query-параметри. Наприклад, параметри пагінації, сортування, фільтрації чи локалі зазвичай змінюють вміст відповіді.
Новий Service Worker не замінює старий миттєво. Типовий життєвий цикл:
браузер завантажує новий файл;
новий Service Worker встановлюється;
він переходить у стан waiting;
після завершення роботи старих вкладок стає активним;
новий Service Worker починає контролювати клієнтів.
Це захищає відкриту сторінку від ситуації, коли частина ресурсів походить зі старої версії, а частина — з нової.
Виклик self.skipWaiting() пришвидшує активацію, але може спричинити змішування версій ресурсів. Його варто використовувати лише разом із продуманою стратегією версіювання та оновлення клієнтів.
Після оновлення кешу часто потрібне повне узгоджене оновлення app shell. Інакше сторінка може завантажити старий index.html, який посилається на вже відсутній або несумісний JavaScript-файл.
Ресурси додаються до кешу під час встановлення Service Worker:
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open("static-v1").then((cache) => {
return cache.addAll([
"/",
"/index.html",
"/app.js",
]);
}),
);
});Якщо хоча б один ресурс не вдасться завантажити, addAll() відхилить Promise. Тому список має містити лише доступні критичні ресурси.
Ресурс додається до кешу після першого фактичного запиту:
const response = await fetch(request);
const cache = await caches.open("runtime-v1");
await cache.put(request, response.clone());
return response;Це зменшує початкову кількість запитів під час встановлення, але ресурс буде доступний офлайн лише після першого завантаження.
Офлайн-підтримка має бути спроєктована на рівні функціональності, а не лише на рівні мережевих запитів.
Потрібно вирішити:
яку сторінку показати без мережі;
які дані можна показувати застарілими;
як повідомити користувачу про офлайн-стан;
що робити з діями, виконаними офлайн;
чи потрібно зберігати невідправлені зміни в IndexedDB;
як синхронізувати зміни після відновлення мережі.
Кешована GET-відповідь не забезпечує автоматичну офлайн-підтримку POST або PUT. Для офлайн-мутацій потрібна окрема черга операцій і логіка повторної відправки.
no-cache і no-storeno-cache дозволяє зберігати відповідь, але вимагає перевірки актуальності.
no-store забороняє зберігання.
Кешувати зазвичай потрібно лише безпечні GET і, за окремих умов, HEAD. Запити, що змінюють стан сервера, не слід обробляти як звичайні кешовані GET-запити.
response.clone()Одна відповідь використовується для повернення браузеру, інша — для запису в Cache API.
Кожна нова назва кешу залишається в браузері, доки код явно її не видалить. Це призводить до накопичення старих ресурсів і витрати дискового простору.
Відповідь користувача A не повинна випадково стати доступною користувачу B. Особливо уважно потрібно працювати з cookie, сесіями та Authorization.
Cache API повертає те, що було записано раніше. Він не виконує автоматичну перевірку через ETag і не знає, коли дані стали застарілими.
Користувач може тривалий час бачити стару інформацію. Для динамічних даних краще підходять network first або stale while revalidate.
fetch не відхиляє Promise для статусів 404 або 500. Потрібно перевіряти response.ok або конкретний статус.
const response = await fetch("/api/data");
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}Service Worker зазвичай контролює лише URL у межах своєї директорії. Файл /scripts/sw.js не матиме автоматичного контролю над усім сайтом без відповідного налаштування області дії.
Офлайн-сценарії потрібно перевіряти в DevTools:
перше завантаження з мережею;
повторне завантаження без мережі;
відсутність ресурсу в кеші;
оновлення Service Worker;
очищення старих кешів;
зміну API-відповіді;
вихід користувача з облікового запису.
Для складного застосунку корисно розділити кеші за призначенням:
const STATIC_CACHE = "static-v4";
const API_CACHE = "api-v3";
const IMAGE_CACHE = "images-v2";Окремі кеші спрощують:
очищення;
аналіз вмісту;
зміну політики;
обмеження розміру;
міграцію між версіями.
Стратегію слід обирати для кожного типу ресурсу окремо:
app shell — cache first;
хешовані статичні файли — cache first;
HTML-навігація — network first;
динамічний API — network first;
часто переглядувані некритичні дані — stale while revalidate;
приватні або чутливі відповіді — без кешування або з дуже обмеженим кешем.
HTTP-кеш керується заголовками Cache-Control, ETag, Last-Modified і Vary.
no-cache означає повторну перевірку, а no-store — заборону зберігання.
Cache API є окремим програмним сховищем пар Request і Response.
Service Worker може перехоплювати запити та реалізовувати офлайн-стратегії.
Cache first підходить для стабільних статичних ресурсів.
Network first підходить для даних, які мають бути актуальними.
Stale while revalidate забезпечує швидку відповідь і фонове оновлення.
Для запису відповіді в кеш потрібно використовувати response.clone().
Старі кеші потрібно видаляти під час активації нової версії Service Worker.
Персональні дані та відповіді, що залежать від автентифікації, не можна кешувати без чіткої моделі безпеки.
Офлайн-кешування GET-запитів не вирішує автоматично проблему офлайн-мутацій.