Cache API - сравнительно старый API для управления хранилищем кэша, доступный уже во всех современных браузерах и являющийся частью ServiceWorker.
Прежде чем мы будем говорить о самом API, немного поговорим про контекст. Когда мы говорим о кэшировании веб-приложений, перед нами несколько (зачастую независимых) путей:
Image Cache
Preload Cache
Cache API
HTTP Cache
Рассказ о каждом из них, ровно, как и сравнение, — отдельная статья. Здесь же я хочу рассказать именно о Cache API.
Представим, что нам необходимо сделать веб-приложение, запрашивающее какие-то данные с сервера. Данные меняются не так часто, поэтому, стоило бы их поместить в кэш, чтобы не обращаться каждый раз к БД. Также хотелось бы, чтобы кэш был более управляем на стороне клиента.
Но и тут путей организации кэширования на стороне клиента несколько:
Local Storage
IndexedDB
Cache Storage
И еще много других ?
А вот тут поговорим о каждом из данных путей:
С Local Storage понятно - устоявшийся стандарт хранилища на стороне клиента, но, как и всегда, есть нюансы - максимальный размер 5 Мб, а еще он синхронен и блокирует основной поток.
IndexedDB - интересное решение. Стоит учитывать, что максимальный размер варьируется в зависимости от браузера, однако этот размер в любом случае больше, чем 5 Мб. Но все упирается в то, что есть следующий метод - Cache Storage, созданный специально для хранения кэша?
Cache Storage - хранилище, созданное специально для кэша. Так же как и в IndexedDB, максимальный размер варьируется в зависимости от браузера.
Также, если вы хотите детальнее узнать о концепциях, лежащих в основе Web Storage - есть прекрасная статья.
Я упомянул о том, что размер Local Storage ограничен 5 Мб, а IndexedDB и Cache Storage - ограничен свободным пространством и различными реализациями в зависимости от браузера, но я не сказал о том, что мы можем узнать, сколько доступно свободного пространства для записи и сколько уже занято.
Для этого есть прекрасное API - Storage Manager. И еще одна новость - он имеет хорошую поддержку в большинстве современных браузеров.
Помимо двух методов, предоставляющих информацию по различным политикам хранения данных на вашем сайте и в браузере в целом, он содержит очень полезный для нас метод - estimate(), возвращающий информацию о свободном и используемом пространстве.
Вот пример, как мы можем посчитать сколько процентов от свободного пространства мы используем и сколько свободно в байтах:

Исходный код
if (navigator.storage && navigator.storage.estimate) { const quota = await navigator.storage.estimate(); // quota.usage -> свободное пространство ( в байтах). // quota.quota -> доступное пространство (в байтах). const percentageUsed = (quota.usage / quota.quota) * 100; console.log(`Вы используете ${percentageUsed}% от доступного пространства.`); const remaining = quota.quota - quota.usage; console.log(`Вы можете записать еще ${remaining} байт.`); }
Итак, подытожим:
Local Storage | IndexedDB | Cache Storage | |
Позволяет хранить большие объемы данных | ❌ | ✅ | ✅ |
Асинхронен | ❌ | ✅ | ✅ |
Удобен для хранения кэша | ❌ | ❌ | ✅ |
Разобрав каждый из методов, давай уже приступим к написанию своего примера. Представим, что нам необходимо реализовать веб-приложение с новостной лентой. Поскольку мы хотим снизить нагрузку на сервер - добавим кэширование. Сами же новости будем забирать с помощью JSONPlaceholder. А рабочий же пример будет в конце.
Для начала, давайте получим данные и добавим их на нашу страницу.

Исходный код
const postFeed = document.querySelector("#postFeed"); const createArticleElement = (title: string, body: string) => ` <article class='article card'> <h2 class='title'>${title}</h2> <p class='text'>${body}</p> </article> `; const addToFeed = (data: { title: string; body: string }[]) => { if (postFeed) { data.forEach(({ title, body }: { title: string; body: string }) => { postFeed.innerHTML += createArticleElement(title, body); }); } }; if (postFeed) { fetch("https://jsonplaceholder.typicode.com/posts").then(async (res) => { const data = await res.json(); addToFeed(data); }); } }
А вот наш HTML

Исходный код
<main id="app"> <div class='feed' id='postFeed'> </div> </main>
В итоге, мы получаем такую страницу:

Если мы зайдем в DevTools и откроем вкладку Network, то заметим, что сейчас запрос на получение новостей происходит на каждое открытие страницы.
Как раз здесь в игру вступает CacheAPI:

Исходный код
(async () => { if (postFeed) { const url = "https://jsonplaceholder.typicode.com/posts"; const cache = await caches.open("cache"); // Открываем кэш. Если такого не было до - создаестся новый. const cachedResponse = await cache.match(url); if (cachedResponse) { const data = await cachedResponse.json(); addToFeed(data); return; } fetch(url).then(async (res) => { cache.put(url, res); const cloned = res.clone(); addToFeed(await cloned.json()); }); } })();
Что здесь добавилось?

Во-первых, мы открываем новый кэш. Если же кэша с таким именем нет - добавится новый.
Исходный код
const cache = await caches.open("cache");

После этого мы можем использовать его, что дальше и делаем: проверяем, есть ли кэш в хранилище по переданному URL запроса. Если есть - просто добавляем значения на страницу.
Исходный код
const cachedResponse = await cache.match(url); if (cachedResponse) { const data = await cachedResponse.json(); addToFeed(data); return; }

Если же кэша нет - запрашиваем с сервера данные и кладем их в кэш по этому запросу и, опять же, добавляем на страницу.
Исходный код
fetch(url).then(async (res): Promise<void> => { cache.put(url, res); const cloned = res.clone(); addToFeed(await cloned.json()); });
Хорошо, с одной проблемой разобрались - теперь мы не будем делать лишние запросы на сервер и храним данные ответа на устройстве клиента. Но есть одна проблема: если мы добавим новую новость, запрос не уйдет на сервер, потому что достанется из кэша, и, следовательно, пользователь ее не увидит. Чтобы решить эту проблему - можно использовать несколько способов, однако, для простоты, я приведу один - в ответе будем возвращать заголовок Expires для удаления кэша, как время истекает.
На клиенте же будем проверять, истекло ли время. Если да - удаляем данные из кэша.

Исходный код
(async () => { if (postFeed) { const url = "https://jsonplaceholder.typicode.com/posts"; const cache = await caches.open("cache"); const cachedResponse = await cache.match(url); if (cachedResponse) { const expiresValue = cachedResponse.headers.get("expires"); const dateValue = cachedResponse.headers.get("date"); const maxAge = 7200000; // 2 часа const exp = expiresValue && expiresValue !== "-1" ? Date.parse(expiresValue) : Date.parse(dateValue ?? "") + maxAge; if (Date.now() < exp) { const data = await cachedResponse.json(); addToFeed(data); return; } cache.delete(url); } fetch(url).then(async (res): Promise<void> => { const cloned = res.clone(); const headers = new Headers([...cloned.headers.entries()]); headers.set("date", new Date().toUTCString()); const { body, ...rest } = res; await cache.put( url, new Response(body, { ...rest, headers, }), ); addToFeed(await cloned.json()); }); } })();
Сначала мы получаем заголовок Expires и Date, они нам пригодятся для того, чтобы определять, просрочен ли кэш. Далее мы определяем дефолтное время жизни токена, если сервер не вернул заголовок Expires - 2 часа. Если время жизни токена еще не просрочено, то берем значение из кэша, если же просрочена - удаляем значение.

Исходный код
if (cachedResponse) { const expiresValue = cachedResponse.headers.get("expires"); const dateValue = cachedResponse.headers.get("date"); const maxAge = 7200000; // 2 часа const exp = expiresValue && expiresValue !== "-1" ? Date.parse(expiresValue) : Date.parse(dateValue ?? "") + maxAge; if (Date.now() < exp) { const data = await cachedResponse.json(); addToFeed(data); return; } cache.delete(url); }
Заметьте, что при добавлении кэша из запроса мы добавляем в заголовки Date. Данный заголовок мы используем, если сервер не вернул Expires.

Исходный код
fetch(url).then(async (res): Promise<void> => { const cloned = res.clone(); const headers = new Headers([...cloned.headers.entries()]); headers.set("date", new Date().toUTCString()); const { body, ...rest } = res; await cache.put( url, new Response(body, { ...rest, headers, }), ); addToFeed(await cloned.json()); });
А вот и рабочий пример.
Также советую почитать больше про Cache Storage API в официальной документации. Или же в упрощенной версии - MDN.
Напоследок хочется сказать, что Cache API - довольно полезная технология, позволяющая использовать дисковое пространство пользователя для кэширования ответов сетевых запросов. Более того, мы можем хранить в кэше видоизмененные ответы, меняя заголовки или тело ответа. Такая гибкость дает свои преимущества, а выбор того, что использовать для кэширования в вашем веб-приложении остается за вами.
