PWA: transformando sua app em instalável
1. Fundamentos de PWA e o Manifesto Web
Uma Progressive Web App (PWA) é uma aplicação web que utiliza tecnologias modernas para oferecer uma experiência semelhante a um aplicativo nativo. Seus três pilares fundamentais são: confiável (carrega instantaneamente mesmo em redes instáveis), rápido (responde rapidamente às interações do usuário) e envolvente (oferece uma experiência imersiva, como um app instalado).
O coração da instalabilidade de uma PWA é o arquivo manifest.json. Este arquivo JSON descreve como a aplicação deve se comportar quando instalada no dispositivo do usuário. As propriedades essenciais incluem:
{
"name": "Minha App PWA",
"short_name": "AppPWA",
"description": "Uma aplicação React instalável",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3f51b5",
"icons": [
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
Para que uma app seja considerada instalável, ela deve atender a três critérios obrigatórios: ser servida via HTTPS, possuir um Service Worker registrado e ter um manifest.json válido.
2. Gerando e Configurando o Manifest no React
No ecossistema React, você pode criar o manifest.json manualmente na pasta public/ ou utilizar plugins que automatizam o processo. Com o Vite, por exemplo, o plugin vite-plugin-pwa simplifica toda a configuração:
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.ico', 'robots.txt'],
manifest: {
name: 'Minha App PWA',
short_name: 'AppPWA',
description: 'App React instalável',
theme_color: '#3f51b5',
background_color: '#ffffff',
display: 'standalone',
orientation: 'portrait',
scope: '/',
start_url: '/',
icons: [
{
src: '/icons/icon-192x192.png',
sizes: '192x192',
type: 'image/png',
purpose: 'any maskable'
},
{
src: '/icons/icon-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any maskable'
}
]
}
})
]
});
A propriedade display: standalone remove a barra de endereço do navegador, criando uma experiência imersiva. Já orientation permite fixar a orientação da tela (retrato ou paisagem).
3. Service Worker: O Coração da PWA
O Service Worker é um script JavaScript que o navegador executa em segundo plano, separado da página web. Ele permite interceptar requisições de rede, gerenciar caches e enviar notificações push. Seu ciclo de vida inclui três eventos principais: instalação, ativação e fetch.
Usando o Workbox (biblioteca do Google para Service Workers), podemos implementar estratégias de cache de forma declarativa:
// sw.js
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
// Pré-cache dos assets da build
precacheAndRoute(self.__WB_MANIFEST);
// Estratégia Cache First para assets estáticos (JS, CSS, fontes)
registerRoute(
({ request }) => request.destination === 'style' ||
request.destination === 'script' ||
request.destination === 'font',
new CacheFirst({
cacheName: 'static-assets',
plugins: [
new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 30 * 24 * 60 * 60 })
]
})
);
// Estratégia Network First para navegação (HTML)
registerRoute(
({ request }) => request.mode === 'navigate',
new NetworkFirst({
cacheName: 'pages',
plugins: [
new ExpirationPlugin({ maxEntries: 5, maxAgeSeconds: 24 * 60 * 60 })
]
})
);
// Estratégia Stale-While-Revalidate para dados de API
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new StaleWhileRevalidate({
cacheName: 'api-data',
plugins: [
new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 5 * 60 })
]
})
);
4. Estratégias de Cache para Aplicações React
Para aplicações React, é crucial definir estratégias específicas para diferentes tipos de recursos:
Cache de assets estáticos: Os bundles gerados pelo React (JS e CSS) mudam a cada build, então podemos usar CacheFirst com nomes únicos (hash).
Cache de dados de API: Para dados dinâmicos, a estratégia NetworkFirst com fallback para cache é ideal. Se a rede falhar, o usuário vê dados em cache:
// Cache de dados da API com fallback offline
registerRoute(
({ url }) => url.pathname.startsWith('/api/produtos'),
new NetworkFirst({
cacheName: 'produtos-cache',
networkTimeoutSeconds: 3,
plugins: [
new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 60 * 60 })
]
})
);
Atualização dinâmica: Para evitar caches obsoletos, versionamos o cache e limpamos versões antigas durante a ativação do Service Worker:
self.addEventListener('activate', (event) => {
const cacheWhitelist = ['static-assets-v2', 'pages-v2', 'api-data-v2'];
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (!cacheWhitelist.includes(cacheName)) {
return caches.delete(cacheName);
}
})
);
})
);
});
5. Tornando a App Instalável: Evento beforeinstallprompt
O evento beforeinstallprompt é disparado quando o navegador detecta que a app atende aos critérios de instalabilidade. Podemos interceptá-lo e exibir um botão customizado:
// InstallButton.jsx
import { useState, useEffect } from 'react';
function InstallButton() {
const [deferredPrompt, setDeferredPrompt] = useState(null);
const [isInstalled, setIsInstalled] = useState(false);
useEffect(() => {
// Verifica se já está instalada
if (window.matchMedia('(display-mode: standalone)').matches) {
setIsInstalled(true);
}
const handler = (e) => {
e.preventDefault();
setDeferredPrompt(e);
};
window.addEventListener('beforeinstallprompt', handler);
return () => window.removeEventListener('beforeinstallprompt', handler);
}, []);
const handleInstall = async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
setIsInstalled(true);
}
setDeferredPrompt(null);
};
if (isInstalled) return null;
return (
<button onClick={handleInstall} className="install-btn">
📲 Instalar App
</button>
);
}
6. Experiência Offline e Sincronização em Background
Para exibir conteúdo offline de forma elegante, criamos um componente indicador:
// OfflineIndicator.jsx
import { useState, useEffect } from 'react';
function OfflineIndicator() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
if (isOnline) return null;
return (
<div className="offline-banner">
⚠️ Você está offline. Alguns dados podem não estar disponíveis.
</div>
);
}
Para sincronização em background, usamos a API Background Sync. Exemplo prático: enviar um formulário offline:
// Registro do sync no Service Worker
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-formulario') {
event.waitUntil(sincronizarFormularios());
}
});
async function sincronizarFormularios() {
const db = await openDB('formularios-offline', 1);
const formularios = await db.getAll('pendentes');
for (const form of formularios) {
try {
await fetch('/api/formulario', {
method: 'POST',
body: JSON.stringify(form),
headers: { 'Content-Type': 'application/json' }
});
await db.delete('pendentes', form.id);
} catch (error) {
console.error('Falha ao sincronizar formulário:', error);
}
}
}
7. Testes e Depuração de PWA no Node.js/React
Para garantir que sua PWA está funcionando corretamente, utilize estas ferramentas:
- Lighthouse: Ferramenta integrada no Chrome DevTools que audita sua PWA e fornece métricas de performance, acessibilidade e boas práticas.
- DevTools > Application: Painel dedicado para inspecionar Manifest, Service Workers, Cache Storage e IndexedDB.
- Teste em dispositivos móveis: Use o modo de dispositivo no DevTools ou o
ngrokpara testar em dispositivos reais via HTTPS.
Para depuração avançada do Service Worker, use postMessage:
// No Service Worker
self.addEventListener('message', (event) => {
if (event.data === 'ping') {
event.ports[0].postMessage('pong');
}
});
// No React
navigator.serviceWorker.controller.postMessage('ping');
8. Boas Práticas e Considerações Finais
Atualização silenciosa: Para evitar que o usuário precise recarregar a página manualmente, use skipWaiting e clients.claim:
self.addEventListener('install', () => {
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(clients.claim());
});
Tratamento de erros: Sempre forneça feedback amigável quando o usuário estiver offline:
// Componente de fallback para dados offline
function DadosOffline() {
return (
<div className="offline-fallback">
<h3>Sem conexão</h3>
<p>Você está offline. Mostrando dados em cache.</p>
<button onClick={() => window.location.reload()}>
Tentar novamente
</button>
</div>
);
}
Performance: Implemente lazy loading de rotas com React Router para pré-carregar apenas o necessário:
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home'));
const Produtos = lazy(() => import('./pages/Produtos'));
const Sobre = lazy(() => import('./pages/Sobre'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Carregando...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/produtos" element={<Produtos />} />
<Route path="/sobre" element={<Sobre />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
Ao seguir estas práticas, sua aplicação React se tornará uma PWA completa, instalável e funcional mesmo offline. Lembre-se de testar em diferentes cenários de rede e dispositivos para garantir a melhor experiência do usuário.
Referências
- Documentação oficial de PWA (MDN Web Docs) — Guia completo sobre Progressive Web Apps, incluindo manifest, Service Workers e APIs relacionadas.
- Workbox: Biblioteca para Service Workers (Google) — Documentação oficial da biblioteca Workbox para gerenciamento de cache e estratégias de Service Worker.
- Vite PWA Plugin — Plugin oficial para adicionar suporte PWA em projetos Vite + React, com configuração simplificada de manifest e Service Worker.
- Lighthouse: Ferramenta de auditoria de PWA (Chrome DevTools) — Guia para usar o Lighthouse e melhorar as métricas de PWA, performance e acessibilidade.
- Background Sync API (W3C) — Especificação oficial da API de sincronização em background para aplicações web offline-first.
- React Router: Lazy Loading e Code Splitting — Documentação oficial sobre carregamento sob demanda de rotas com React Router para otimização de performance.