Cache API e offline-first
TL;DR
A Cache API armazena pares Request/Response — diferente de localStorage (strings) e IndexedDB (objetos), ela guarda respostas HTTP completas. É usada principalmente dentro de Service Workers para implementar estratégias de cache de assets e dados da API. Junto com IndexedDB, é a base de apps offline-first e PWAs.
Cache API
// Abrir (ou criar) um cache por nome
const cache = await caches.open('v1');
// Adicionar um único URL (faz o fetch internamente)
await cache.add('/styles/app.css');
// Adicionar múltiplos URLs (falha se qualquer um falhar)
await cache.addAll([
'/',
'/styles/app.css',
'/scripts/app.js',
'/offline.html',
]);
// Salvar um par Request/Response manualmente
const response = await fetch('/api/products');
await cache.put('/api/products', response.clone()); // clone porque response é stream única
// Buscar do cache
const cached = await cache.match('/styles/app.css');
// cached: Response ou undefined
// Buscar em todos os caches
const fromAny = await caches.match('/styles/app.css');
// Listar todas as chaves
const requests = await cache.keys();
// Deletar entrada
await cache.delete('/api/products');
// Deletar cache inteiro
await caches.delete('v1');
// Listar caches disponíveis
const cacheNames = await caches.keys();Cache em Service Workers
O uso mais comum da Cache API é dentro de Service Workers. O Service Worker intercepta requests e decide se serve do cache ou da rede.
// sw.js
const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = [
'/',
'/app.css',
'/app.js',
'/offline.html',
];
// Install: pré-cachear assets estáticos
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS))
);
self.skipWaiting(); // ativar imediatamente sem esperar aba fechar
});
// Activate: limpar caches antigos
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(names =>
Promise.all(
names
.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
)
)
);
self.clients.claim(); // controlar abas imediatamente
});Estratégias de cache
Cache First (melhor para assets estáticos imutáveis)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(cached => {
return cached ?? fetch(event.request);
})
);
});Network First (melhor para dados da API)
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then(response => {
// Atualizar cache com resposta fresca
const clone = response.clone();
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
return response;
})
.catch(() => caches.match(event.request)) // fallback ao cache se offline
);
});Stale-While-Revalidate (melhor para dados que mudam mas aceitam staleness)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open(CACHE_NAME).then(async cache => {
const cached = await cache.match(event.request);
// Buscar nova versão em background (não aguardar)
const fetchPromise = fetch(event.request).then(response => {
cache.put(event.request, response.clone());
return response;
});
// Retornar cache imediatamente; na próxima visita estará atualizado
return cached ?? fetchPromise;
})
);
});Cache Only (para assets que nunca mudam)
self.addEventListener('fetch', (event) => {
event.respondWith(caches.match(event.request));
});Estratégia por tipo de recurso
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Assets estáticos versionados (/app.abc123.js) — Cache First
if (url.pathname.match(/\.[a-f0-9]{8}\.(js|css)$/)) {
event.respondWith(cacheFirst(event.request));
return;
}
// API de dados — Network First
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(event.request));
return;
}
// Páginas HTML — Stale-While-Revalidate
if (event.request.mode === 'navigate') {
event.respondWith(staleWhileRevalidate(event.request));
return;
}
});Página offline
// Fallback para página offline quando navegação falha
self.addEventListener('fetch', (event) => {
if (event.request.mode !== 'navigate') return;
event.respondWith(
fetch(event.request).catch(() => caches.match('/offline.html'))
);
});<!-- offline.html -->
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<title>Sem conexão</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>Você está offline</h1>
<p>Verifique sua conexão com a internet e tente novamente.</p>
<button onclick="location.reload()">Tentar novamente</button>
</body>
</html>Workbox — abstração de alto nível
Workbox (Google) abstrai as estratégias acima em configuração declarativa:
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
// Assets JS/CSS — Cache First com expiração de 30 dias
registerRoute(
({ request }) => request.destination === 'script' || request.destination === 'style',
new CacheFirst({
cacheName: 'static-assets',
plugins: [new ExpirationPlugin({ maxAgeSeconds: 30 * 24 * 60 * 60 })],
})
);
// Imagens — Cache First com limite de 50 entradas
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [new ExpirationPlugin({ maxEntries: 50 })],
})
);
// API — Network First com fallback
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({ cacheName: 'api-cache', networkTimeoutSeconds: 3 })
);Para fixar
- Qual a diferença entre a Cache API e o cache HTTP padrão do browser?
- Por que você precisa de
response.clone()antes de salvar no cache?- Quando você usaria Cache First vs Network First vs Stale-While-Revalidate?
- O que
self.skipWaiting()faz no Service Worker? Quando é importante usar?- O que acontece com caches de versões antigas se você não os limpar no evento
activate?
Veja também
- 02 — IndexedDB — anterior
- 04 — Storage em entrevista — próxima e capstone
- Workers 03 — Service Workers — context completo do Service Worker