Service Worker (public/sw.js)
O front-web-base registra um Service Worker de ~456 linhas que cacheia
documentos de navegação, não só assets. É a camada de cache mais próxima do
usuário e a única que vive dentro do browser dele — fora do alcance de
qualquer purge do Cloudflare.
:::tip Se alguém reporta "HTML velho depois do deploy"
Comece por aqui. Um purge de zona, um manual-cache-purge.yml e um bump de
CACHE_GENERATION não alcançam o cache do Service Worker. O box do SW é
rotacionado por BUILD_ID, então um deploy normal resolve — mas se o
/sw.js do usuário ficou preso, ou se a página serviu stale dentro da janela de
frescor, o sintoma é exatamente "HTML velho" com todas as camadas de servidor
já limpas. Ver Diagnóstico.
:::
O que ele cacheia
| Caminho | Estratégia | Chave |
|---|---|---|
| Navegação (documento HTML) | SWR + gate de frescor | query colapsada (ver abaixo) |
/api/games/* e *.data | SWR + gate de frescor | request completa |
/assets/* | cache-first | request completa |
request.destination === "image" | cache-first, limite de 1000 entradas | request completa |
/offline.html | pré-cacheado no install | — |
/clever (landing de afiliado) é a exceção explícita: sempre vai à rede,
porque o loader dela processa os params de afiliado a cada carga.
A chave de navegação colapsa a query
Esta é a parte que mais surpreende. cacheKeyUrl():
// public/sw.js
function cacheKeyUrl(rawUrl) {
const url = new URL(rawUrl);
const version = url.searchParams.get("version");
const versionSuffix = version ? `&version=${encodeURIComponent(version)}` : "";
return `${url.origin}${url.pathname}?__build-id=${BUILD_ID}${versionSuffix}`;
}
- Toda a query é descartada —
/,/?a=1e/?utm_source=xcolapsam na mesma entrada. Sem bloat, e o F5 de uma landing com UTM bate na mesma chave que já está cacheada. ?version=Xé o BYPASS DE EMERGÊNCIA. É o único param preservado na chave:/?version=2pega uma entrada distinta de/. Use quando precisar forçar um usuário específico a sair de um documento cacheado.__build-idé um param SINTÉTICO, só de chave — nunca vai pra rede. Build novo → chave nova → entrada velha inalcançável.
O colapso vale só pra navegação. /api/* e .data (dataSWR) keiam pela
request completa, então params de conteúdo (q, category, paginação)
continuam preservados lá.
Na primeira carga (MISS) o SW busca a request original, com query, pra o
loader root capturar o tracking, e grava sob a chave limpa. Redirects
(redirected) não são cacheados, então a atribuição de clean-URL é preservada.
Gate de frescor + SWR
navigationSWR não é um SWR puro:
- HIT dentro do TTL da config (
idade < freshnessTtl): serve o HTML do cache, zero rede. O cache fica "impregnado" por até o valor deSSR_CACHE_HTML_TTLque a origem carimbou noCache-Control. - HIT passado o TTL: serve o stale já (0ms) e revalida em background com
If-None-Match: <ETag>— 304 barato mantém a cópia, 200 substitui pra próxima. - MISS: rede, cacheia; rede falha →
/offline.html.
A idade vem do header Date da resposta cacheada; a janela de frescor vem do
Cache-Control (max-age tem precedência sobre s-maxage; sem nenhum → 0,
sempre revalida). Ou seja: o TTL do documento no SW é o mesmo TTL que a borda
declarou — não há número hardcoded aqui.
Há uma eviction deliberada no caminho de revalidação: um 200 autoritativo que
não é mais text/html (ex: /robots.txt que passou a sair no-store da
borda) evicta a entrada. Sem isso a cópia stale era servida em toda navegação
até o box do build morrer — caso real de bet7k.cl servindo Disallow: /
antigo indefinidamente no Ctrl+R.
Isolamento per-usuário: pelas RESPOSTAS, não por cookie
Não existe bypass por auth no fetch handler, de propósito: um Service
Worker não consegue ler o header Cookie (é forbidden-header —
request.headers.get("Cookie") vem sempre vazio), então checar
is_authenticated/jwt_token na request seria no-op. Foi um bug histórico, já
removido.
A isolação depende das respostas: a borda (workers/middleware.ts) marca todo
documento / .data / api autenticado como private, no-store, e
isCacheableNavigation / isCacheableData respeitam isso → conteúdo per-usuário
nunca entra no cache do SW.
Os shells públicos são auth-agnósticos (o SSR não carrega dado de usuário — ver Fluxo de autenticação), então servir um shell cacheado a um usuário logado é seguro: ele hidrata o auth client-side. Logados podem usar o cache do SW — é justamente o objetivo do cache instantâneo.
Guardas de isCacheableNavigation: só 200/301/308, só text/html, só
same-origin não-redirecionado (type === "basic", !redirected), e nunca com
Cache-Control: no-store.
Versionamento e limpeza dos boxes
const CACHE_NAME = "app-apcsrasj-v6-__SW_VERSION__";
__SW_VERSION__ é substituído por BUILD_ID em build time pelo
swVersionPlugin (vite.config.ts, hook writeBundle → build/client/sw.js).
O token apcsrasj-v6 é um literal: um Service Worker roda no browser sem
acesso ao env do wrangler, então ele não acompanha o CACHE_GENERATION do
front-ops/config/cache/generation.yml. Não confie nele como indicador de
geração — a invalidação real é o BUILD_ID, que rotaciona em todo deploy.
Cada build é um "box" (um Cache do CacheStorage). A limpeza roda em três
momentos:
activate— deleta todos os caches cujo nome difere do atual, e purga entradas corrompidas (status non-2xx) do cache corrente.- No
fetch, throttled — no máximo 1× por hora (BOX_CLEANUP_INTERVAL_MS). - Sob demanda — mensagem
cleanup-old-boxesda página (ignora o throttle).
O SW chama self.skipWaiting() no install e self.clients.claim() no
activate: todas as abas trocam pro SW novo em segundos, sem exigir reload.
Registro
// app/entry.client.tsx
navigator.serviceWorker.register(`/sw.js?build=${BUILD_ID}`).catch(() => {});
Roda depois da hidratação (via requestIdleCallback, fallback 4s) pra não
competir com o React por CPU. O ?build=<BUILD_ID> faz o browser tratar o
script como atualizado quando o build muda — garante a rotação do box mesmo se o
revalidate do script travar. A query não muda o scope (continua /) nem quebra
TWA/PWA.
/sw.js nunca é cacheado
workers/middleware.ts intercepta o pathname /sw.js e força
Cache-Control: no-cache, must-revalidate — nem no cache assets:, nem com
immutable. O browser precisa re-checar o script a cada visita pra pegar o
build novo; um sw.js velho preso no HTTP cache travaria a atualização e a
limpeza dos boxes antigos.
Prefetch de documento sob demanda
A página pode mandar uma mensagem cache-document (ver
app/components/perf/PrefetchCurrentDocument.tsx) pedindo ao SW pra buscar o
HTML da rota atual sem os params de tracking e gravar sob a mesma chave
normalizada que a navegação usa. O React Router só cacheia o .data na
navegação SPA; isto cobre o HTML que só entraria num F5. Se já estiver
cacheado, não refaz.
Diagnóstico e escape hatches
| Ferramenta | O que faz |
|---|---|
app/utils/version-info.client.ts | collectVersionInfo() reporta serviceWorker: "controlado" | "registrado" | "nenhum", versão do servidor (fetchServerVersion) e probes de HTML/.data (probeCurrent) |
app/utils/force-clear-cache.client.ts | forceClearCache() deleta todos os caches + desregistra os SWs; prepareFreshNextLoad() faz o mesmo preparando a próxima carga |
app/utils/cache-purge.client.ts | purgeAllClientCaches() — chamado no logout (via useAuthLogoutCleanup) |
Roteiro pra um report de "vejo conteúdo velho":
- Confirme o estado do SW —
collectVersionInfo()(serviceWorker+BUILD_IDdo cliente vs. do servidor). BUILD_ID divergente = o usuário está num box antigo. - Ainda divergente depois de reload? É o
/sw.jspreso ou o box não rotacionado —forceClearCache()resolve no cliente. - BUILD_ID igual mas conteúdo velho? O SW está servindo dentro da janela de
frescor que a borda declarou. O lever é
SSR_CACHE_HTML_TTLdo ambiente, não o SW. Ver Cache architecture. - Precisa desviar um usuário específico agora? Mande ele abrir com
?version=2— bypass de chave, sem tocar em nada no servidor.
Docs relacionadas
- Cache strategy (não-técnico)
- Cache architecture (técnico)
- Cache operations
- Fluxo de autenticação — por que o shell público é auth-agnóstico