Aplicativos web modernos geralmente são projetados com a suposição de que os usuários sempre terão uma conexão estável com a Internet. Na realidade, as interrupções na rede são comuns. Um usuário pode perder a conectividade durante uma viagem, ter um sinal móvel fraco ou abrir um aplicativo em um local com acesso limitado à Internet.
Quando um aplicativo depende inteiramente de solicitações de rede ativas, mesmo uma falha temporária de conexão pode impedir que os usuários acessem informações importantes ou concluam uma tarefa.
As tecnologias Progressive Web App (PWA) fornecem ferramentas para enfrentar esses desafios. Com service workers, a API Cache e o armazenamento do navegador, os desenvolvedores podem criar aplicativos que permanecem úteis quando a conectividade é limitada ou temporariamente indisponível.
Construir uma experiência offline não significa que todos os recursos devam funcionar sem um servidor. Em vez disso, o objetivo é identificar quais recursos e tarefas podem permanecer disponíveis off-line, comunicar claramente as limitações e sincronizar as informações com segurança quando a conectividade retornar.
Este tutorial explora técnicas práticas para criar aplicações web mais resilientes usando tecnologias de navegador padrão.
1. Entenda o que realmente significa suporte offline
O suporte offline não é um recurso único do navegador. É uma combinação de cache de recursos, gerenciamento de dados, design de interface e estratégias de sincronização.
Por exemplo, considere um aplicativo de gerenciamento de tarefas. Sua interface, folhas de estilo e ícones podem ser armazenados localmente para que o aplicativo possa ser aberto sem conexão com a Internet. As tarefas baixadas anteriormente também podem permanecer disponíveis para leitura.
No entanto, criar uma nova tarefa e sincronizá-la com uma conta remota requer lógica adicional. O aplicativo deve decidir como armazenar a alteração temporariamente e o que fazer se a sincronização falhar.
Um ponto de partida útil é dividir a funcionalidade do aplicativo em três categorias:
-
Recursos prontos para uso off-line: Arquivos estáticos como HTML, CSS, JavaScript, fontes e ícones.
-
Dados disponíveis localmente: Informações baixadas anteriormente que os usuários podem precisar sem conexão.
-
Operações dependentes da rede: Ações que exigem um serviço remoto, como recuperar preços em tempo real ou processar um pagamento.
Esta distinção ajuda os desenvolvedores a estabelecer expectativas realistas. Uma interface em cache não disponibiliza automaticamente todos os recursos offline, e os dados armazenados localmente não devem ser tratados como um substituto para os registros oficiais do servidor.
2. Use um Service Worker para armazenar recursos essenciais em cache
Um service worker é um arquivo JavaScript executado separadamente da página principal e pode interceptar solicitações de rede elegíveis. Ele fornece a base para muitas experiências off-line na web.
Os service workers geralmente exigem um contexto seguro, como HTTPS, embora os navegadores tratem o localhost como um contexto seguro para desenvolvimento.
O exemplo a seguir armazena em cache um pequeno shell de aplicativo durante a instalação.
Crie um arquivo chamado sw.js:
const CACHE_NAME = "task-app-v1";
const APP_SHELL = (
"/",
"/index.html",
"/styles.css",
"/app.js",
"/offline.html"
);
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(APP_SHELL);
})
);
});
Quando o service worker é instalado, ele abre um cache nomeado e armazena os recursos listados.
O cache.addAll() O método rejeita se um recurso necessário não puder ser armazenado em cache com êxito. Esse comportamento ajuda a impedir que a instalação seja bem-sucedida silenciosamente quando um arquivo essencial estiver faltando.
O exemplo pressupõe que todos os recursos listados existam no caminho especificado. A página offline deve ser um arquivo HTML real e o shell do aplicativo deve conter os recursos necessários para renderizar a interface básica.
Em seguida, registre o service worker no arquivo JavaScript principal do aplicativo:
if ("serviceWorker" in navigator) {
window.addEventListener("load", async () => {
try {
await navigator.serviceWorker.register("/sw.js");
console.log("Service worker registered.");
} catch (error) {
console.error(
"Service worker registration failed:",
error
);
}
});
}
Registrar o trabalhador após o carregamento da página evita tornar a configuração do trabalhador de serviço parte do caminho de renderização imediato da página.
Um registro bem-sucedido não significa necessariamente que o trabalhador já esteja controlando a página atual. Instalação, ativação e controle são estágios separados no ciclo de vida do service worker.
Para aplicativos de produção, os desenvolvedores também devem planejar o controle de versão e a limpeza do cache para que recursos desatualizados não permaneçam indefinidamente.
3. Servir recursos em cache quando a rede estiver indisponível
Armazenar arquivos em cache durante a instalação é apenas parte do processo. A aplicação também deve decidir como as solicitações devem ser tratadas.
Uma estratégia de cache primeiro verifica o cache local antes de tentar uma solicitação de rede. Geralmente é adequado para recursos estáticos com versão que não precisam ser atualizados a cada visita.
Adicione o seguinte manipulador de eventos ao sw.js:
self.addEventListener("fetch", (event) => {
const request = event.request;
const url = new URL(request.url);
if (
request.method !== "GET" ||
url.origin !== self.location.origin
) {
return;
}
if (url.pathname.startsWith("/api/")) {
return;
}
event.respondWith(
caches.match(request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(request);
})
);
});
Este manipulador usa respostas armazenadas em cache para corresponder a solicitações da mesma origem. Se um recurso correspondente não estiver armazenado em cache, ele tentará recuperá-lo da rede.
As solicitações de API são excluídas dessa estratégia específica porque os dados do aplicativo geralmente exigem regras diferentes de atualização e sincronização.
Há uma limitação importante: se um recurso não estiver armazenado em cache e a rede estiver indisponível, a solicitação de rede ainda poderá falhar. Uma implementação mais completa pode retornar um substituto offline para solicitações de navegação adequadas.
Por exemplo, um substituto de navegação pode ser adicionado dentro do manipulador de busca:
event.respondWith(
caches.match(request).then(async (cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
try {
return await fetch(request);
} catch {
if (request.mode === "navigate") {
return (
await caches.match("/offline.html")
) ?? Response.error();
}
return Response.error();
}
})
);
Use este substituto no lugar do anterior event.respondWith() bloco, não como um segundo manipulador para a mesma solicitação.
O substituto destina-se a solicitações de navegação. Ele não deve retornar uma página HTML para um arquivo JavaScript, imagem ou resposta de API porque esses recursos esperam conteúdo diferente.
4. Escolha uma estratégia de cache que corresponda aos dados
Cache-first não é a melhor abordagem para todas as solicitações.
Um site de notícias, por exemplo, pode querer exibir os artigos mais recentes em cache enquanto tenta recuperar conteúdo atualizado do servidor. Um aplicativo que exibe inventário que muda frequentemente pode precisar evitar a apresentação de dados antigos como atuais.
Uma estratégia que prioriza a rede tenta recuperar uma nova resposta e recorre a uma cópia em cache quando a solicitação de rede falha.
Uma implementação simplificada fica assim:
async function networkFirst(request) {
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open("data-v1");
await cache.put(request, response.clone());
}
return response;
} catch (error) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
throw error;
}
}
Este auxiliar pode ser usado por um service worker para solicitações GET apropriadas.
Ele armazena em cache respostas HTTP bem-sucedidas e usa uma resposta previamente armazenada em cache se a solicitação de rede for rejeitada. O response.clone() call é necessária porque os corpos de resposta são fluxos que geralmente não podem ser consumidos mais de uma vez.
No entanto, esta estratégia simplificada não implementa expiração, limites de tamanho de cache ou metadados de atualização. Uma resposta armazenada em cache também pode conter informações que não são mais válidas.
Os desenvolvedores devem, portanto, decidir por quanto tempo os diferentes tipos de dados podem permanecer no cache e se os usuários precisam de uma indicação de que as informações estão obsoletas.
Para aplicações autenticadas, as respostas privadas requerem cuidados especiais. Nunca presuma que todas as respostas da API podem ser armazenadas com segurança em um cache compartilhado ou persistente.
5. Detecte a conectividade sem depender dela
O navegador fornece o navigator.onLine propriedade e eventos online/offline.
Isso pode ajudar um aplicativo a atualizar sua interface quando o navegador relatar uma alteração na conectividade.
Por exemplo:
const connectionStatus = document.querySelector(
"#connection-status"
);
function updateConnectionStatus() {
connectionStatus.textContent = navigator.onLine
? "Connection available"
: "You may be offline";
}
window.addEventListener(
"online",
updateConnectionStatus
);
window.addEventListener(
"offline",
updateConnectionStatus
);
updateConnectionStatus();
O exemplo assume que a página contém um elemento com o ID connection-status.
A distinção entre a conectividade do navegador e a disponibilidade real do serviço é importante. navigator.onLine é apenas uma dica útil. Um dispositivo pode estar conectado ao Wi-Fi enquanto o roteador não tem acesso à Internet, ou uma API específica pode estar indisponível mesmo que outros sites funcionem.
Por esse motivo, os aplicativos ainda devem lidar diretamente com falhas de solicitação de rede.
Uma interface útil pode mostrar um pequeno aviso off-line enquanto permite que os usuários continuem lendo as informações armazenadas em cache. Deve-se evitar alegar que uma operação foi bem-sucedida simplesmente porque o navegador informa que a conectividade foi retornada.
6. Armazene alterações off-line com segurança
Alguns aplicativos precisam oferecer suporte a ações enquanto os usuários estão offline.
Imagine um trabalhador de campo usando um aplicativo de gerenciamento de tarefas em uma área com pouca conectividade. O trabalhador deve ser capaz de registrar uma atualização de tarefa sem perdê-la quando a conexão desaparecer.
Para dados estruturados, o IndexedDB é geralmente mais adequado do que depender de localStorage sozinho. IndexedDB oferece suporte a operações assíncronas e armazena registros estruturados, tornando-o útil para coleções maiores de dados offline.
Um exemplo simples de abertura de um banco de dados é:
function openTaskDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open("task-app", 1);
request.onupgradeneeded = () => {
const database = request.result;
if (!database.objectStoreNames.contains("pending")) {
database.createObjectStore("pending", {
keyPath: "id"
});
}
};
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
Este código cria um armazenamento de objetos chamado pending na primeira vez que o banco de dados é inicializado. O id propriedade serve como chave para cada registro.
Uma atualização offline pode ser representada como um objeto contendo um identificador exclusivo, a operação pretendida e as informações necessárias para a sincronização.
A próxima etapa seria gravar esse objeto no banco de dados e tentar novamente a sincronização quando apropriado.
Isto requer mais do que detectar o online evento. O aplicativo deve gerenciar operações duplicadas, autenticação expirada, falhas de validação de servidor e alterações conflitantes.
Por exemplo, se uma tarefa for editada em dois dispositivos diferentes enquanto estiver off-line, o aplicativo precisará de uma política de resolução de conflitos. Dependendo do produto, isso pode envolver verificações de versão no servidor, mesclar campos independentes ou solicitar ao usuário que resolva alterações conflitantes.
7. Trate a sincronização em segundo plano como uma melhoria
Alguns navegadores oferecem suporte à sincronização em segundo plano, que pode permitir que um prestador de serviço tente trabalhar novamente quando a conectividade retornar.
No entanto, o suporte varia entre navegadores e plataformas. Não deve ser o único mecanismo responsável pela entrega de dados importantes do usuário.
Um aplicativo resiliente pode manter uma fila de operações pendentes no IndexedDB e tentar a sincronização quando o aplicativo for iniciado ou quando uma página relevante se tornar ativa.
O processo de sincronização deve:
Recuperar operações pendentes do armazenamento local.
Envie cada operação para o servidor.
Remova uma operação somente depois que o servidor confirmar o sucesso.
Retenha falhas repetíveis para uma tentativa posterior.
Descubra falhas permanentes que exigem intervenção do usuário.
Para operações que alteram o estado do servidor, o backend também deve suportar um mecanismo apropriado de prevenção de duplicatas. Um identificador de operação exclusivo ou chave de idempotência pode ajudar a evitar que a mesma ação seja aplicada duas vezes após um resultado de rede incerto.
O cliente nunca deve presumir que uma solicitação expirada falhou no servidor. O servidor pode ter processado a operação mesmo que a resposta não tenha chegado ao navegador.
8. Teste o comportamento offline antes do lançamento
É fácil subestimar o suporte offline se os testes ocorrerem apenas em uma conexão de desenvolvimento estável.
Um plano de testes prático deve incluir os seguintes cenários:
-
Abra o aplicativo online e recarregue-o com a rede desativada.
-
Visite uma rota que não foi armazenada em cache.
-
Solicite dados que nunca foram armazenados localmente.
-
Envie uma atualização off-line e reconecte.
-
Simule um erro de servidor em vez de uma interrupção completa da rede.
-
Teste conteúdo obsoleto em cache e versões atualizadas de aplicativos.
-
Verifique se a falha na sincronização não exclui as alterações pendentes.
-
Verifique o comportamento nos navegadores e dispositivos suportados pela aplicação.
As ferramentas de desenvolvedor de navegador podem ajudar a simular condições off-line e inspecionar registros e caches de prestadores de serviços.
Os desenvolvedores também devem lembrar que o armazenamento do navegador pode ser limpo ou despejado. O suporte offline não substitui os backups do servidor e informações críticas não devem existir apenas no cache do navegador.
Conclusão
Experiências off-line confiáveis exigem decisões ponderadas sobre quais recursos armazenar em cache, quais dados podem ser armazenados localmente e como as alterações devem ser sincronizadas.
Os service workers possibilitam interceptar solicitações e servir recursos armazenados em cache. A API Cache fornece uma maneira de armazenar respostas, enquanto o IndexedDB oferece suporte a dados offline estruturados. Os eventos de conectividade podem melhorar o feedback da interface, mas as solicitações reais ainda devem ser tratadas como operações potencialmente não confiáveis.
A estratégia offline mais eficaz depende da finalidade do aplicativo. Um site de conteúdo pode priorizar a leitura em cache, enquanto um aplicativo de produtividade pode precisar de uma fila durável de alterações pendentes e de um processo claro de resolução de conflitos.
Ao projetar para falhas de rede desde o início, os desenvolvedores podem criar aplicações web que permanecem úteis em mais situações, sem enganar os usuários sobre o que foi ou não salvo.




