Menyimpan dan memuat status game dengan IndexedDB
IndexedDB adalah pilihan terbaik untuk menyimpan data game secara persisten di browser. IndexedDB dapat menangani data dalam jumlah besar, bekerja secara offline, dan tidak memblokir thread utama.
1) Mengapa memilih IndexedDB daripada localStorage?
| Fitur | localStorage | IndexedDB |
|---|---|---|
| Batas penyimpanan | ~5–10 MB | 50+ MB (sering kali GB) |
| Jenis data | Hanya string | Objek, blob, array |
| Asinkron | Tidak (memblokir) | Ya |
| Kueri terindeks | Tidak | Ya |
Untuk game, IndexedDB hampir selalu menjadi pilihan yang tepat.
2) Membuka database
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
// Create stores
if (!db.objectStoreNames.contains('saves')) {
db.createObjectStore('saves', { keyPath: 'slot' })
}
if (!db.objectStoreNames.contains('settings')) {
db.createObjectStore('settings', { keyPath: 'key' })
}
}
})
}3) Menyimpan status game
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) Memuat status game
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) Menampilkan semua penyimpanan
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) Menghapus penyimpanan
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) Class GameStorage yang lengkap
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) Menyimpan data biner (tekstur, audio)
IndexedDB dapat menangani Blob dan ArrayBuffer:
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) Pola simpan otomatis
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('Autosaved')
}
}, this.interval)
}
stop() {
clearInterval(this.timer)
}
}10) Penanganan error dan fallback
async function safeLoad(slot, defaultState) {
try {
const saved = await loadGame(slot)
if (saved) {
// Validate/migrate old saves if needed
return migrateSave(saved)
}
} catch (err) {
console.warn('Failed to load save:', err)
}
return defaultState
}
function migrateSave(save) {
// Handle old save formats
if (!save.version) {
save.version = 1
save.settings = save.settings || {}
}
return save
}11) Mempertahankan penyimpanan agar tidak terhapus
IndexedDB tidak dijamin akan selalu tersimpan. Secara default, sebuah origin menggunakan penyimpanan "best-effort", dan browser dapat menghapusnya saat ruang disk penuh atau, pada Safari/WebKit, setelah situs Anda tidak berinteraksi dengan pengguna selama jangka waktu tertentu. Untuk penyimpanan game, data semacam inilah yang tentu tidak ingin Anda hilangkan.
Minta penyimpanan persisten agar browser tidak menghapus data Anda tanpa tindakan eksplisit dari pengguna:
async function makeStoragePersistent() {
if (navigator.storage && navigator.storage.persist) {
const persisted = await navigator.storage.persist()
console.log(persisted ? 'Saves are protected from eviction' : 'Saves may be evicted under storage pressure')
return persisted
}
return false
}Browser menentukan apakah permintaan tersebut dikabulkan berdasarkan sinyal interaksi (seberapa sering pengguna berinteraksi dengan situs Anda, apakah situs dipasang sebagai PWA), jadi jangan berasumsi bahwa permintaan ini selalu berhasil. Anda juga dapat memeriksa ruang yang tersedia sebelum menulis penyimpanan berukuran besar atau aset yang di-cache:
async function checkStorage() {
if (navigator.storage && navigator.storage.estimate) {
const { usage, quota } = await navigator.storage.estimate()
console.log(`Using ${usage} of ${quota} bytes`)
}
}Kedua API tersebut memerlukan konteks aman (HTTPS atau localhost).
Terkait
- PWA untuk game offline
- Service worker untuk caching game
- Merilis game web yang dimuat dengan cepat
- Pemuatan aset secara streaming — menggunakan IndexedDB sebagai cache aset
- Analitik untuk game web — melacak pola penyimpanan/pemuatan untuk memahami perilaku pemain
Sumber Eksternal
- MDN: API IndexedDB — referensi API lengkap
- MDN: Menggunakan IndexedDB — panduan langkah demi langkah
- Library idb — wrapper IndexedDB kecil berbasis Promise