Ir para o conteúdo
HexaTransfer
Voltar ao blog
Analises tecnicas

Service Worker e Cache de Ficheiros para Transferência Offline

Use Service Workers para transferência offline de ficheiros. Estratégias de cache, background sync e padrões de progressive web app.

Os Service Workers permitem que uma app de transferência de ficheiros continue a funcionar quando a rede cai: coloque em cache a shell HTML e JS com a Cache API, coloque em fila os carregamentos falhados com a Background Sync API, armazene fragmentos parciais no IndexedDB, e reproduza tudo quando a conectividade regressar. O worker corre numa thread separada com o seu próprio event loop, intercepta eventos fetch para o seu âmbito, e persiste mesmo após fechar o separador. Para ferramentas de upload, a receita certa é uma estratégia stale-while-revalidate para a app shell, fila de fragmentos suportada por IndexedDB para transferências em curso, e um registo de Background Sync periódico que tenta novamente os carregamentos a cada 15 minutos até terem sucesso.

O Que os Service Workers Fazem Concretamente para Apps de Transferência

O grande ganho é que navigator.serviceWorker sobrevive a recarregamentos de separadores, períodos offline e até ao sono do telemóvel. Quando um utilizador inicia um carregamento de 2 GB num Wi-Fi instável de comboio, quer que os fragmentos que já tiveram sucesso permaneçam bem-sucedidos, que os que estão em curso retomem na reconexão, e que todo o estado seja recuperável se o browser fechar o separador para recuperar memória. Um Service Worker — que corre independentemente de qualquer separador específico — é a peça que permite tudo isso.

A API oferece três blocos de construção: Cache para armazenar respostas por URL, IndexedDB para dados estruturados (filas de fragmentos, estado de sessão), e SyncManager para agendar tentativas que disparam quando o dispositivo está online.

Registar e Versionar o Worker

Registar uma vez no carregamento da app e tratar as atualizações explicitamente:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js', { scope: '/' })
    .then((reg) => reg.addEventListener('updatefound', () => {
      const sw = reg.installing;
      sw.addEventListener('statechange', () => {
        if (sw.state === 'installed' && navigator.serviceWorker.controller) {
          // nova versão pronta, pedir ao utilizador para atualizar
        }
      });
    }));
}

Versione as chaves de cache (transfer-v7) para que um novo deploy invalide os recursos antigos sem deixar JavaScript obsoleto para trás. O bug clássico: index.html fica em cache para sempre, os utilizadores nunca recebem a atualização, e fica preso a depurar via Slack durante semanas. Fixe caches a hashes de build e limpe no evento activate.

Estratégias de Cache para App Shell vs Dados do Utilizador

Recursos diferentes merecem estratégias diferentes:

  • App shell (HTML, CSS, JS, ícones): cache-first com fallback para rede. Carregamentos instantâneos, funciona offline.
  • Metadados de API (/shares/:id): network-first com fallback para cache, TTL de 60 segundos. Atualizado quando online, utilizável quando não.
  • Bytes de ficheiros: nunca em cache. Os ficheiros são frequentemente multi-gigabyte e a Cache API tem quotas por origem (tipicamente 60% do disco livre).
  • Fontes de CDNs: stale-while-revalidate. Rápido e auto-refrescante.

No handler fetch:

self.addEventListener('fetch', (e) => {
  const url = new URL(e.request.url);
  if (url.pathname.startsWith('/assets/')) {
    e.respondWith(cacheFirst(e.request, 'shell-v7'));
  } else if (url.pathname.startsWith('/api/shares/')) {
    e.respondWith(networkFirst(e.request, 'api-v1', 60));
  }
});

Nunca intercete pedidos de carregamentos binários de ficheiros — encaminhe à volta deles verificando e.request.method === 'PUT' e retornando cedo. Fazer proxy de PUTs de gigabytes pelo worker é um desastre de memória.

Colocar em Fila Carregamentos Falhados com Background Sync

SyncManager é a chave para carregamentos resilientes. Quando um PUT de fragmento falha, guarde-o no IndexedDB e registe um sync:

// no código da página
const reg = await navigator.serviceWorker.ready;
await reg.sync.register('flush-uploads');
// em sw.js
self.addEventListener('sync', (event) => {
  if (event.tag === 'flush-uploads') {
    event.waitUntil(flushPendingUploads());
  }
});

O browser dispara o evento sync quando a rede regresa, com backoff exponencial até ~24 horas. Chrome e Edge suportam-no; o Safari lançou um subconjunto no 17.5 com a flag "Background Fetch" mas requer permissão do utilizador. Para Safari, faça fallback para tentar novamente na próxima abertura de página visível via visibilitychange.

A Background Fetch API é uma ferramenta separada especificamente para operações de ficheiros grandes — mostra uma notificação persistente na UI do browser para que os utilizadores possam acompanhar o progresso mesmo após fechar o separador. Vale a pena usar para carregamentos acima de 500 MB.

Armazenar Carregamentos Parciais no IndexedDB

O IndexedDB é o espaço de rascunho durável. Abra uma base de dados pequena uma vez, depois guarde metadados de sessão mais offsets de fragmentos:

const db = await openDB('transfers', 1, {
  upgrade(db) {
    db.createObjectStore('sessions', { keyPath: 'id' });
    db.createObjectStore('chunks', { keyPath: ['sessionId', 'index'] });
  }
});
await db.put('sessions', {
  id, fileName, fileSize, fileFingerprint, createdAt: Date.now(),
  completedIndexes: [], partUrls
});

Não armazene bytes de fragmentos em bruto — vêm do handle File, que o IndexedDB pode persistir como referência de clone estruturado que permanece válida após recarregamentos. Armazenar o handle evita duplicar 2 GB de bytes na base de dados.

O armazenamento por origem tem limites: cerca de 60% do disco livre no Chrome de desktop, 1 GB por origem no iOS Safari antes de a pressão de ejeção começar. Peça navigator.storage.persist() para obter o bucket "persistente" que os browsers evitam ejetar automaticamente.

Tratar Transições Offline e Online

Escute eventos online e offline, tanto na página como no service worker:

// página
window.addEventListener('online', () => {
  ui.showBanner('De volta online — a retomar carregamentos');
  navigator.serviceWorker.controller?.postMessage({ type: 'resume' });
});
window.addEventListener('offline', () => {
  ui.showBanner('Offline — carregamentos pausados');
});

navigator.onLine é notoriamente pouco fiável em portais cativos corporativos — reporta true quando o dispositivo tem uma ligação de rede local mas sem internet. Para deteção confiável, faça um fetch('/ping', { cache: 'no-store' }) pequeno com um timeout de 3 segundos.

Torná-la uma PWA Adequada

Envie um manifest.json com display: standalone, um conjunto de ícones e start_url: /. Adicione links apple-touch-icon para iOS. Declare o tratamento de ficheiros para que o SO possa associar a app a extensões específicas:

{
  "name": "Hex Transfer",
  "file_handlers": [{
    "action": "/share-target",
    "accept": { "application/*": [".pdf", ".zip", ".docx"] }
  }]
}

Combinado com Web Share Target, isto permite aos utilizadores partilhar ficheiros da folha de partilha do SO diretamente para a app. No Chrome Android e Chromium de desktop, a PWA pode registar-se como handler padrão para os tipos de ficheiro declarados. Isto transforma uma página de browser numa app que se comporta como uma ferramenta de upload nativa.

Testar o Cenário Offline

Três cenários a testar manualmente, porque os testes offline automatizados são instáveis:

  1. Iniciar um carregamento de 500 MB em Wi-Fi rápido, mudar para modo avião a 30%, aguardar 30 segundos, reativar o Wi-Fi. O carregamento deve retomar de onde parou sem ação do utilizador.
  2. Iniciar um carregamento, fechar o separador a 60%, aguardar 2 minutos, reabrir. Oferecer para retomar a sessão.
  3. Iniciar um carregamento no telemóvel, bloquear o ecrã por 5 minutos. O background sync deve disparar quando desbloquear e terminar a transferência.

A caixa de verificação "Offline" do Chrome DevTools e Application > Service Workers > Update on reload são indispensáveis. Os perfis de "Throttling" do painel Network permitem simular Fast 3G e Slow 3G para ver como a UI de erros se comporta.

A app web do HexaTransfer usa um Service Worker para cache da app-shell e IndexedDB para estado de sessão em curso, para que recarregamentos e breves períodos offline não percam o progresso de carregamento. Experimente em hexatransfer.com — gratuito, sem conta necessária, máximo de 10 GB.

Armadilhas a Conhecer

Os Service Workers têm um pequeno conjunto de problemas que apanham os principiantes: só funcionam em HTTPS (exceto localhost), as quotas de cache variam muito entre browsers, o iOS Safari não acorda workers de forma fiável para Background Sync, o DevTools pode colocar em cache workers obsoletos de forma agressiva (clique sempre em "Bypass for network" durante o desenvolvimento), e importScripts corre de forma síncrona durante a instalação por isso nunca carregue scripts lentos de terceiros aí. Escreva um pequeno teste de integração que verifique que o worker ativa, reclama clientes e serve a página offline — esse único teste apanha 80% das regressões que encontrará em produção.

Envie arquivos grandes com segurança e criptografia de ponta a ponta

Transfira arquivos de até 10 GB gratuitamente com criptografia de ponta a ponta. Sem necessidade de conta. Seus arquivos são criptografados no navegador antes do envio — ninguém mais pode lê-los.

Enviar um arquivo