Pular para o conteúdo
~/.primo-academy.sh
☰ Aulas · PWA: Service Workers, estrategias de cache, IndexedDB, background sync e offline-first · 0/7
Recomendado: essencial

Estrategias de cache: cache first, network first, SWR, Workbox

6 min de leitura

fonte

Voce registrou o Service Worker. Agora vem a decisao que define o UX offline do seu PWA: qual estrategia de cache usar pra cada tipo de recurso?

A escolha errada faz o usuario ver conteudo desatualizado quando devia ver o atual (ou o contrario). Cache first pra API data = usuario ve versao de ontem, mesmo online. Network first pra imagens = app lento offline. Sao tradeoffs reais, e cada tipo de recurso pede uma estrategia diferente.

Se voce entende as 4 estrategias canonicas (cache first, network first, SWR, cache only), quando usar cada uma, e como o Workbox abstrai isso, voce sai de "cache tudo" pra "cache certo pro tipo certo de recurso".

O essencial 🟢

As 4 estrategias canonicas. O Workbox define 4 estrategias basicas (alem de compor elas):

EstrategiaCache hitCache missQuando usar
Cache Firstserve cachebusca rede, cacheaAssets imutaveis (JS, CSS com hash)
Network Firstbusca redeserve cache (fallback)API data, frescor importa
Stale-While-Revalidateserve cache (stale)busca rede em background, atualiza cacheConteudo que muda devagar
Cache Onlyserve cacheerro (404)Assets estaticos pre-cacheados

Cache First - "cache vence, rede atualiza". Padrão: serve do cache se tiver. Se nao, vai pra rede e cachea o resultado.

// Vanilla
self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      return cached || fetch(event.request).then((response) => {
        return caches.open("v1").then((cache) => {
          cache.put(event.request, response.clone());
          return response;
        });
      });
    })
  );
});
// Workbox
import { CacheFirst } from "workbox-strategies";

registerRoute(
  ({ request }) => request.destination === "image",
  new CacheFirst({
    cacheName: "images",
    plugins: [{
      expiration: { maxEntries: 50, maxAgeSeconds: 30 * 24 * 60 * 60 },
    }],
  })
);

Use pra: assets com hash (JS, CSS gerado pelo build, fonts, imagens estaticas). Cache vence → instantaneo. Se nao tem, rede cacheia pra proxima vez.

Risco: se o conteudo muda (CSS sem hash, API data), o usuario ve versao antiga mesmo online. Solução: nome do cache versionado + invalidação no activate.

Network First - "rede vence, cache fallback". Padrão: vai pra rede. Se falha (offline, timeout), serve do cache.

import { NetworkFirst } from "workbox-strategies";

registerRoute(
  ({ url }) => url.pathname.startsWith("/api/"),
  new NetworkFirst({
    cacheName: "api",
    networkTimeoutSeconds: 3,  // timeout rapido
    plugins: [{
      expiration: { maxEntries: 50, maxAgeSeconds: 5 * 60 },
    }],
  })
);

Use pra: API data, feed, perfil de usuario. Frescor importa mais que velocidade. O timeout rapido (3s) garante que em conexao ruim cai pro cache rapido (nao espera 30s pelo timeout do browser).

Tradeoff: em conexao lenta, o usuario espera a rede. Use com parcimonia - so pra dados que realmente precisam ser fresh.

Stale-While-Revalidate (SWR) - "cache imediato, rede atualiza em background". O melhor dos dois mundos: serve do cache instantaneamente, mas em paralelo busca na rede pra atualizar o cache pra proxima vez.

import { StaleWhileRevalidate } from "workbox-strategies";

registerRoute(
  ({ url }) => url.pathname.startsWith("/static/"),
  new StaleWhileRevalidate({
    cacheName: "static-content",
    plugins: [{
      expiration: { maxEntries: 100, maxAgeSeconds: 24 * 60 * 60 },
    }],
  })
);

Fluxo:

  1. Request chega no SW.
  2. SW checa cache → hit: serve do cache.
  3. Em paralelo, SW faz fetch na rede.
  4. Response da rede chega → cache atualiza.
  5. Proxima request: cache ja tem versao nova.

Use pra: conteudo que muda devagar mas voce quer fresh na proxima visita. Lista de posts, configuracoes, profile publico, documentacao.

Risco: o usuario sempre ve a versao antiga (ate a rede atualizar). Nao use pra dados criticos (preco, saldo bancario).

Cache Only - "cache ou nada". Serve so do cache. Se nao tem, retorna erro (404). Util pra assets pre-cacheados que voce sabe que vai ter (app shell):

import { CacheOnly } from "workbox-strategies";

registerRoute(
  ({ url }) => url.pathname === "/offline.html",
  new CacheOnly({ cacheName: "app-shell" })
);

Combinado com pre-cache no install, garante que /offline.html sempre existe (e o usuario ve uma pagina "voce esta offline" em vez de um 404 cru).

Network Only - "rede sempre". NUNCA cacheia. Util pra POSTs/PUTs/DELETEs (nao faz sentido cachear mutações):

import { NetworkOnly } from "workbox-strategies";

registerRoute(
  ({ request }) => request.method === "POST",
  new NetworkOnly()
);

Ou pra endpoints que precisam ser fresh (analytics, tracking, dashboards de metricas).

A decision tree: qual estrategia usar. Dada a natureza do recurso, a escolha e' quase mecanica:

Decision tree: dado o tipo de recurso, escolha a estrategia. Regra pratica: 80% dos casos = SWR ou Network First. Cache First so pra assets com hash (build output). Cache Only so pra pre-cache do install.

ExpirationPlugin - o "limpa o cache periodicamente". Sem limite, o cache cresce ate estourar a cota do browser. Sempre configure:

import { ExpirationPlugin } from "workbox-expiration";

new CacheFirst({
  cacheName: "images",
  plugins: [
    new ExpirationPlugin({
      maxEntries: 50,  // max 50 items
      maxAgeSeconds: 30 * 24 * 60 * 60,  // 30 dias
      purgeOnQuotaError: true,  // limpa se cota estourar
    }),
  ],
});

maxEntries: 50 = no maximo 50 imagens. maxAgeSeconds: 30 dias = remove items mais velhos. purgeOnQuotaError: true = se o browser avisar "cota estourando", o Workbox limpa o cache pro app nao quebrar.

CacheableResponsePlugin - so cachea responses 2xx. Por default, SW cacheia qualquer response (incluindo 404, 500). CacheableResponsePlugin filtra:

import { CacheableResponsePlugin } from "workbox-cacheable-response";

new NetworkFirst({
  cacheName: "api",
  plugins: [
    new CacheableResponsePlugin({ statuses: [0, 200] }),
    // 0 = opaque (cross-origin sem CORS)
    // 200 = OK
    // NAO cachear 404, 500, etc
  ],
});

statuses: [0, 200] so cacheia OK. Sem isso, respostas 404 ficam em cache e causam "404 fantasmas" mesmo quando o servidor voltou.

Plugins do Workbox - a toolbox. Alem de Expiration e CacheableResponse, Workbox oferece:

  • BackgroundSyncPlugin - retry de POSTs falhados.
  • BroadcastUpdatePlugin - notifica pagina quando cache atualiza.
  • CacheKeyWillBeUsedCallback - custom cache keys.
  • RequestWillBeFetchedCallback - custom fetch options.

Pra maioria dos apps, Expiration + CacheableResponse cobrem 90%.

Precache vs runtime cache. Sao 2 conceitos diferentes:

  • Precache: assets pre-cacheados no install event. Sempre no cache desde o primeiro load. Usado pra app shell.
  • Runtime cache: responses cacheados sob demanda no fetch event. Voce so tem o asset se ja visitou a pagina.
// Precache (no install)
self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open("app-shell-v1").then((cache) =>
      cache.addAll(["/", "/app.js", "/styles.css", "/offline.html"])
    )
  );
});

// Runtime cache (no fetch)
registerRoute(
  ({ request }) => request.destination === "image",
  new CacheFirst({ cacheName: "images" })
);

Workbox InjectManifest plugin faz automaticamente o precache do build output (self.__WB_MANIFEST).

Aprofundamento 🟡

BroadcastChannel - SW ↔ pagina comunicacao em tempo real. Alem de postMessage (one-way), use BroadcastChannel pra eventos:

// SW
const channel = new BroadcastChannel("cache-updates");
self.addEventListener("fetch", (event) => {
  // ... estrategia
  channel.postMessage({
    type: "CACHE_UPDATED",
    url: event.request.url,
  });
});

// Pagina
const channel = new BroadcastChannel("cache-updates");
channel.addEventListener("message", (event) => {
  if (event.data.type === "CACHE_UPDATED") {
    // Mostra "Nova versao disponivel" ou recarrega
  }
});

Util pra avisar a pagina que dados atualizaram (e.g., SW buscou versao nova de uma API e a UI pode re-renderizar).

staleWhileRevalidate com plugins: [ExpirationPlugin] - o combo classico para assets de build. Workbox recomenda SWR + Expiration pra assets de build (JS, CSS, fonts):

import { registerRoute } from "workbox-routing";
import { StaleWhileRevalidate } from "workbox-strategies";
import { ExpirationPlugin } from "workbox-expiration";

registerRoute(
  ({ request }) =>
    ["script", "style", "font"].includes(request.destination),
  new StaleWhileRevalidate({
    cacheName: "build-assets",
    plugins: [{
      expiration: {
        maxEntries: 60,
        maxAgeSeconds: 30 * 24 * 60 * 60,
      },
    }],
  })
);

Assets de build raramente mudam, mas quando mudam (deploy), voce quer a versao nova. SWR garante: usuario nunca espera (cache hit), cache atualiza em background.

NetworkFirst vs CacheFirst com precache do build. Diferenca sutil: comprecache do build (vite-plugin-pwa faz automaticamente), os assets estao sempre no cache. CacheFirst e NetworkFirst dao o mesmo resultado pratico. Use StaleWhileRevalidate por default, exceto pra assets que nao mudam nunca (imagens, fonts) - use CacheFirst.

Range requests e streaming. SW nao suporta streaming response nativamente. Pra video, audio, ou downloads grandes, o SW quebra o streaming. Workaround: deixe esses requests passarem (sem interceptar):

self.addEventListener("fetch", (event) => {
  // Range requests: deixa passar
  if (event.request.headers.has("range")) return;

  // Ou: streaming (video, audio)
  if (event.request.destination === "video" ||
      event.request.destination === "audio") {
    return;  // nao intercepta
  }

  // ... estrategia
});

request.mode = "no-cors" e opaque responses. Requests cross-origin sem CORS retornam opaque responses (status 0, body inacessivel). CacheableResponsePlugin com statuses: [0, 200] cacheia opacas. Use pra CDN de imagens sem CORS (ex: unsplash, placeholder services).

Range requests em video. Video HTML5 usa range requests pra streaming. SW deve deixar passar. Sem isso, video nao carrega (ou carrega inteiro, sem streaming).

Pra quem quer ir mais alem 🔴

StaleWhileRevalidate e a opcao de atualizacao. O SWR sempre busca na rede, mesmo se cache existe. Se voce quer economizar banda (mobile 3G), use cacheFirst com verificacao periodica de update. Pattern:

// Manual SWR
self.addEventListener("fetch", (event) => {
  event.respondWith(
    (async () => {
      const cached = await caches.match(event.request);
      const networkPromise = fetch(event.request).then((response) => {
        if (response.ok) {
          caches.open("v1").then((cache) =>
            cache.put(event.request, response.clone())
          );
        }
        return response;
      });

      // Retorna cache imediatamente, network atualiza
      return cached || networkPromise;
    })()
  );
});

Equivalente ao SWR, mas voce controla quando chamar network (so se nao tem cache). Mais complexo, raramente vale.

MaxAgeSeconds e a politica de TTL. O maxAgeSeconds e' relativo a data de cache (nao a data de Last-Modified do recurso). Se voce cachear um response com maxAgeSeconds: 3600, apos 1h da data de cache, expira. Independente se o recurso no servidor mudou. Pra TTL baseado no servidor, use Cache-Control: max-age=3600 no response + plugin customizado.

precache em Vite via vite-plugin-pwa. O plugin automaticamente faz precache de todos os assets do build (JS, CSS, HTML, imagens em /public). Lista injetada como self.__WB_MANIFEST:

// sw.js gerado pelo vite-plugin-pwa
import { precacheAndRoute } from "workbox-precaching";

precacheAndRoute(self.__WB_MANIFEST);

Cada build tem hash unico (app-abc123.js). Workbox detecta novos arquivos e atualiza o precache automaticamente.

workbox-broadcast-update - notifica clientes quando cache atualiza. Plugin que envia BroadcastChannel quando uma URL em cache e' atualizada:

import { BroadcastUpdatePlugin } from "workbox-broadcast-update";

new StaleWhileRevalidate({
  cacheName: "content",
  plugins: [new BroadcastUpdatePlugin()],
});

Pagina escuta o evento, mostra "Nova versao disponivel". UX avancado.

Leitura recomendada:

Dica: o erro mais comum em cache strategies e' usar a mesma estrategia pra tudo. "Cache first pra tudo" = dados stale online. "Network first pra tudo" = app lento offline. Cada tipo de recurso pede uma estrategia diferente. A regra pratica: SWR pra assets de build e conteudo semi-estatico, Network First pra API data, Cache First pra assets com hash, Cache Only pra pre-cache do app shell. Nao invente - existe 4 estrategias canonicas, combine conforme o tipo.

No proximo no, vamos IndexedDB: a "base de dados" do browser. API nativa versus Dexie (wrapper moderno), queries com indices, e migrations. Cobre como armazenar dados offline-first que persistem entre sessoes.

// Quiz

Quando usar Stale-While-Revalidate (SWR) ao inves de Network First ou Cache First?

Escolha uma alternativa

// recursos

// avaliação da trilha

—
ainda sem avaliações