PWA para jogos web offline
Os Progressive Web Apps permitem que os jogadores instalem seu jogo e joguem offline. Este tutorial mostra como adicionar suporte a PWA a um jogo web.
1) Manifesto do aplicativo web
Crie manifest.json:
{
"name": "Meu Jogo Incrível",
"short_name": "JogoIncrível",
"description": "Um jogo incrível que você pode jogar offline",
"start_url": "/",
"display": "fullscreen",
"orientation": "landscape",
"background_color": "#1a1a2e",
"theme_color": "#4ade80",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/icons/icon-maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}Vincule-o no seu HTML:
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#4ade80">
<link rel="apple-touch-icon" href="/icons/icon-192.png">2) Service worker básico
Crie sw.js:
const CACHE_NAME = 'game-v1'
const ASSETS = [
'/',
'/index.html',
'/game.js',
'/style.css',
'/assets/sprites.png',
'/assets/sounds/jump.mp3',
'/assets/sounds/music.mp3',
]
// Instalação: armazena os recursos em cache
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(ASSETS)
})
)
})
// Ativação: limpa caches antigos
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => {
return Promise.all(
keys.filter(key => key !== CACHE_NAME)
.map(key => caches.delete(key))
)
})
)
})
// Requisição: fornece o conteúdo do cache e recorre à rede se necessário
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request)
})
)
})3) Registre o service worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js')
console.log('SW registered:', registration.scope)
} catch (err) {
console.error('SW registration failed:', err)
}
})
}4) Cache primeiro, com atualização pela rede
Melhor para jogos — carregamento rápido com atualizações em segundo plano:
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open(CACHE_NAME).then(async (cache) => {
const cached = await cache.match(event.request)
// Inicia a requisição de rede em segundo plano
const fetchPromise = fetch(event.request).then((response) => {
if (response.ok) {
cache.put(event.request, response.clone())
}
return response
}).catch(() => null)
// Retorna imediatamente o conteúdo em cache ou aguarda a rede
return cached || fetchPromise
})
)
})5) Cache versionado para atualizações
const CACHE_VERSION = 'v2'
const STATIC_CACHE = `static-${CACHE_VERSION}`
const DYNAMIC_CACHE = `dynamic-${CACHE_VERSION}`
const STATIC_ASSETS = [
'/',
'/index.html',
'/game.js',
// ... recursos essenciais que raramente mudam
]
self.addEventListener('install', (event) => {
self.skipWaiting() // Ativa imediatamente
event.waitUntil(
caches.open(STATIC_CACHE).then(cache => cache.addAll(STATIC_ASSETS))
)
})
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys => {
return Promise.all(
keys.filter(key => !key.includes(CACHE_VERSION))
.map(key => caches.delete(key))
)
})
)
clients.claim() // Assume o controle imediatamente
})6) Como lidar com atualizações do jogo
Avise os jogadores quando houver uma atualização disponível:
// No aplicativo principal
let refreshing = false
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (!refreshing) {
refreshing = true
showUpdateNotification()
}
})
function showUpdateNotification() {
const banner = document.createElement('div')
banner.innerHTML = `
<p>Jogo atualizado! Recarregue para obter a versão mais recente.</p>
<button onclick="location.reload()">Recarregar</button>
`
banner.className = 'update-banner'
document.body.appendChild(banner)
}7) Detecção do modo offline
function updateOnlineStatus() {
if (navigator.onLine) {
hideOfflineBanner()
syncGameData()
} else {
showOfflineBanner()
}
}
window.addEventListener('online', updateOnlineStatus)
window.addEventListener('offline', updateOnlineStatus)
function showOfflineBanner() {
document.getElementById('offline-banner').style.display = 'block'
}
function hideOfflineBanner() {
document.getElementById('offline-banner').style.display = 'none'
}8) Prompt de instalação
let deferredPrompt = null
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault()
deferredPrompt = e
showInstallButton()
})
function showInstallButton() {
const btn = document.getElementById('install-btn')
btn.style.display = 'block'
btn.addEventListener('click', installApp)
}
async function installApp() {
if (!deferredPrompt) return
deferredPrompt.prompt()
const { outcome } = await deferredPrompt.userChoice
console.log('Install prompt outcome:', outcome)
deferredPrompt = null
document.getElementById('install-btn').style.display = 'none'
}
window.addEventListener('appinstalled', () => {
console.log('App installed!')
deferredPrompt = null
})No iOS e no iPadOS, o evento beforeinstallprompt nunca é disparado, portanto o botão de instalação acima só aparece em navegadores baseados no Chromium. Usuários de iPhone e iPad fazem a instalação tocando em Compartilhar e depois em "Adicionar à Tela de Início". Uma mudança ocorrida em 2026 que vale a pena conhecer: a partir do Safari 26 (iOS 26 / iPadOS 26), todo site adicionado à Tela de Início é aberto como um aplicativo web por padrão, com a opção "Abrir como Aplicativo Web" ativada por padrão. Não há mais um requisito de instalabilidade no iOS, embora disponibilizar um manifesto e um service worker ainda proporcione uma experiência offline muito melhor.
9) Sincronização em segundo plano para placares
Atenção ao suporte: a API Background Sync só está implementada em navegadores baseados no Chromium (Chrome, Edge, Opera e Samsung Internet). O Firefox e o Safari (inclusive no iOS) não oferecem suporte a ela, portanto sempre verifique a disponibilidade do recurso e mantenha uma alternativa que envie as pontuações no próximo carregamento normal do aplicativo.
// No service worker
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-scores') {
event.waitUntil(syncScores())
}
})
async function syncScores() {
const db = await openDB('game', 1)
const pendingScores = await db.getAll('pending-scores')
for (const score of pendingScores) {
try {
await fetch('/api/scores', {
method: 'POST',
body: JSON.stringify(score),
headers: { 'Content-Type': 'application/json' }
})
await db.delete('pending-scores', score.id)
} catch (err) {
// Tentará novamente na próxima sincronização
break
}
}
}
// No aplicativo principal
async function submitScore(score) {
try {
await fetch('/api/scores', { method: 'POST', body: JSON.stringify(score) })
} catch {
// Salva para sincronizar depois
const db = await openDB('game', 1)
await db.add('pending-scores', { ...score, id: Date.now() })
if ('serviceWorker' in navigator && 'sync' in window.registration) {
await navigator.serviceWorker.ready
await registration.sync.register('sync-scores')
}
}
}10) Testes da PWA
Chrome DevTools:
- Application > Service Workers
- Application > Manifest
- Application > Cache Storage
- Network > caixa de seleção Offline
Auditoria do Lighthouse:
- Execute a auditoria de PWA
- Verifique a instalabilidade
- Verifique a capacidade de funcionar offline
Testes em dispositivo real:
- Instale na tela inicial do celular
- Ative o modo avião
- Teste todos os recursos offline
Conteúdo relacionado
- Service workers para armazenar jogos em cache
- Saves de jogos com IndexedDB
- Publique um jogo web que carrega rapidamente
- Jogos web adaptados a dispositivos móveis — controles por toque e gerenciamento da viewport em jogos PWA
- Carregamento de recursos por streaming — estratégias de carregamento que funcionam com o cache do service worker
Recursos externos
- MDN: Progressive Web Apps — documentação completa sobre PWA
- web.dev: Aprenda PWA — trilha de aprendizagem sobre PWA do Google
- MDN: Manifesto de Aplicativo Web — referência do arquivo de manifesto