Salve e carregue o estado do jogo com IndexedDB
IndexedDB é a melhor opção para armazenar dados persistentes de jogos no navegador. Ele lida com grandes volumes de dados, funciona offline e não bloqueia a thread principal.
1) Por que usar IndexedDB em vez de localStorage?
| Recurso | localStorage | IndexedDB |
|---|---|---|
| Limite de armazenamento | ~5-10 MB | 50+ MB (geralmente GB) |
| Tipos de dados | Apenas strings | Objetos, blobs, arrays |
| Assíncrono | Não (bloqueia) | Sim |
| Consultas indexadas | Não | Sim |
Para jogos, IndexedDB quase sempre é a escolha certa.
2) Abrindo um banco de dados
function openGameDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MyGame', 1)
request.onerror = () => reject(request.error)
request.onsuccess = () => resolve(request.result)
request.onupgradeneeded = (event) => {
const db = event.target.result
// Cria os armazenamentos
if (!db.objectStoreNames.contains('saves')) {
db.createObjectStore('saves', { keyPath: 'slot' })
}
if (!db.objectStoreNames.contains('settings')) {
db.createObjectStore('settings', { keyPath: 'key' })
}
}
})
}3) Salvando o estado do jogo
async function saveGame(slot, gameState) {
const db = await openGameDB()
return new Promise((resolve, reject) => {
const tx = db.transaction('saves', 'readwrite')
const store = tx.objectStore('saves')
const saveData = {
slot,
state: gameState,
timestamp: Date.now(),
}
const request = store.put(saveData)
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
}4) Carregando o estado do jogo
async function loadGame(slot) {
const db = await openGameDB()
return new Promise((resolve, reject) => {
const tx = db.transaction('saves', 'readonly')
const store = tx.objectStore('saves')
const request = store.get(slot)
request.onsuccess = () => resolve(request.result?.state || null)
request.onerror = () => reject(request.error)
})
}5) Listando todos os saves
async function listSaves() {
const db = await openGameDB()
return new Promise((resolve, reject) => {
const tx = db.transaction('saves', 'readonly')
const store = tx.objectStore('saves')
const request = store.getAll()
request.onsuccess = () => resolve(request.result)
request.onerror = () => reject(request.error)
})
}6) Excluindo um save
async function deleteSave(slot) {
const db = await openGameDB()
return new Promise((resolve, reject) => {
const tx = db.transaction('saves', 'readwrite')
const store = tx.objectStore('saves')
const request = store.delete(slot)
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
}7) Uma classe GameStorage completa
class GameStorage {
constructor(dbName = 'GameData', version = 1) {
this.dbName = dbName
this.version = version
this.db = null
}
async init() {
this.db = await this.openDB()
}
openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version)
request.onerror = () => reject(request.error)
request.onsuccess = () => resolve(request.result)
request.onupgradeneeded = (e) => {
const db = e.target.result
if (!db.objectStoreNames.contains('saves')) {
db.createObjectStore('saves', { keyPath: 'slot' })
}
if (!db.objectStoreNames.contains('settings')) {
db.createObjectStore('settings', { keyPath: 'key' })
}
if (!db.objectStoreNames.contains('assets')) {
db.createObjectStore('assets', { keyPath: 'url' })
}
}
})
}
async save(slot, data) {
const tx = this.db.transaction('saves', 'readwrite')
tx.objectStore('saves').put({ slot, data, timestamp: Date.now() })
return tx.complete
}
async load(slot) {
const tx = this.db.transaction('saves', 'readonly')
const result = await this.promisify(tx.objectStore('saves').get(slot))
return result?.data || null
}
async setSetting(key, value) {
const tx = this.db.transaction('settings', 'readwrite')
tx.objectStore('settings').put({ key, value })
}
async getSetting(key, defaultValue = null) {
const tx = this.db.transaction('settings', 'readonly')
const result = await this.promisify(tx.objectStore('settings').get(key))
return result?.value ?? defaultValue
}
promisify(request) {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result)
request.onerror = () => reject(request.error)
})
}
}8) Armazenando dados binários (texturas, áudio)
IndexedDB lida com Blobs e ArrayBuffers:
async function cacheAsset(url, blob) {
const tx = db.transaction('assets', 'readwrite')
tx.objectStore('assets').put({ url, blob, cached: Date.now() })
}
async function getCachedAsset(url) {
const tx = db.transaction('assets', 'readonly')
const result = await promisify(tx.objectStore('assets').get(url))
return result?.blob || null
}9) Padrão de salvamento automático
class AutoSave {
constructor(storage, interval = 60000) {
this.storage = storage
this.interval = interval
this.timer = null
this.dirty = false
}
markDirty() {
this.dirty = true
}
start(getState) {
this.timer = setInterval(async () => {
if (this.dirty) {
await this.storage.save('autosave', getState())
this.dirty = false
console.log('Salvo automaticamente')
}
}, this.interval)
}
stop() {
clearInterval(this.timer)
}
}10) Tratamento de erros e alternativas
async function safeLoad(slot, defaultState) {
try {
const saved = await loadGame(slot)
if (saved) {
// Valida/migra saves antigos, se necessário
return migrateSave(saved)
}
} catch (err) {
console.warn('Falha ao carregar o save:', err)
}
return defaultState
}
function migrateSave(save) {
// Lida com formatos antigos de save
if (!save.version) {
save.version = 1
save.settings = save.settings || {}
}
return save
}11) Faça os saves sobreviverem à remoção
Não há garantia de que os dados do IndexedDB permanecerão armazenados. Por padrão, uma origem usa armazenamento de "melhor esforço", e o navegador pode removê-lo quando o disco fica cheio ou, no Safari/WebKit, após um período sem interação do usuário com seu site. No caso de saves de jogos, esses são justamente os dados que você não quer perder.
Solicite armazenamento persistente para que o navegador não apague seus dados sem uma ação explícita do usuário:
async function makeStoragePersistent() {
if (navigator.storage && navigator.storage.persist) {
const persisted = await navigator.storage.persist()
console.log(persisted ? 'Os saves estão protegidos contra remoção' : 'Os saves podem ser removidos sob pressão de armazenamento')
return persisted
}
return false
}Os navegadores decidem se concedem essa permissão com base em sinais de engajamento — quanto o usuário interage com seu site e se ele está instalado como PWA —, portanto, não presuma que a solicitação sempre terá sucesso. Você também pode verificar quanto espaço está disponível antes de gravar saves grandes ou assets em cache:
async function checkStorage() {
if (navigator.storage && navigator.storage.estimate) {
const { usage, quota } = await navigator.storage.estimate()
console.log(`Usando ${usage} de ${quota} bytes`)
}
}Ambas as APIs exigem um contexto seguro (HTTPS ou localhost).
Conteúdo relacionado
- PWA para jogos offline
- Service workers para cache de jogos
- Publique um jogo web que carregue rapidamente
- Carregamento de assets por streaming — usando IndexedDB como cache de assets
- Análise de dados para jogos web — acompanhando padrões de salvamento e carregamento para entender o comportamento dos jogadores
Recursos externos
- MDN: API IndexedDB — referência completa da API
- MDN: Como usar IndexedDB — guia passo a passo
- Biblioteca idb — um pequeno wrapper de IndexedDB baseado em Promises