Game asset caching के लिए service workers
Service workers आपको यह control करने देते हैं कि assets कैसे cache और serve हों. Games के लिए इसका मतलब है दोबारा आने पर तुरंत load होना और offline support.
1) Service worker की बुनियादी बातें
अपने root में sw.js बनाएं:
// sw.js
self.addEventListener('install', (event) => {
console.log('Service worker installing')
})
self.addEventListener('activate', (event) => {
console.log('Service worker activated')
})
self.addEventListener('fetch', (event) => {
// Intercept all network requests
})इसे register करें:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW registered'))
.catch(err => console.error('SW registration failed:', err))
}2) Cache-first strategy (game assets के लिए सबसे अच्छी)
Cache से load करें, और सिर्फ तभी fetch करें जब cache में न हो:
const CACHE_NAME = 'game-assets-v1'
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) {
return cached
}
return fetch(event.request).then((response) => {
// Don't cache non-ok responses or non-GET requests
if (!response.ok || event.request.method !== 'GET') {
return response
}
// Clone response for caching
const clone = response.clone()
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, clone)
})
return response
})
})
)
})3) ज़रूरी assets को precache करें
Install event precaching के लिए एकदम सही है:
const CACHE_NAME = 'game-v1'
const PRECACHE = [
'/',
'/index.html',
'/game.js',
'/style.css',
'/assets/sprites.png',
'/assets/tileset.png',
'/assets/player.png',
]
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(PRECACHE))
.then(() => self.skipWaiting())
)
})4) Dynamic content के लिए network-first
जो चीज़ें बदलती रहती हैं (leaderboards, player data) उनके लिए network-first इस्तेमाल करें:
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url)
// API calls: network first
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(event.request))
return
}
// Assets: cache first
event.respondWith(cacheFirst(event.request))
})
async function networkFirst(request) {
try {
const response = await fetch(request)
const cache = await caches.open('api-cache')
cache.put(request, response.clone())
return response
} catch {
return caches.match(request)
}
}
async function cacheFirst(request) {
const cached = await caches.match(request)
if (cached) return cached
const response = await fetch(request)
const cache = await caches.open(CACHE_NAME)
cache.put(request, response.clone())
return response
}5) Stale-while-revalidate
Cache वाली copy तुरंत लौटाएं, और background में update करें:
async function staleWhileRevalidate(request) {
const cache = await caches.open(CACHE_NAME)
const cached = await cache.match(request)
const fetchPromise = fetch(request).then((response) => {
if (response.ok) {
cache.put(request, response.clone())
}
return response
})
return cached || fetchPromise
}6) Cache versioning और cleanup
Activate पर पुराने caches साफ़ करें:
const CACHE_VERSION = 'v2'
const CACHE_NAME = `game-${CACHE_VERSION}`
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => {
return Promise.all(
keys
.filter(key => key.startsWith('game-') && key !== CACHE_NAME)
.map(key => {
console.log('Deleting old cache:', key)
return caches.delete(key)
})
)
}).then(() => self.clients.claim())
)
})7) Type के हिसाब से अलग-अलग caches
बेहतर management के लिए caches को व्यवस्थित करें:
const CACHES = {
static: 'static-v1', // HTML, JS, CSS
images: 'images-v1', // Sprites, textures
audio: 'audio-v1', // Music, SFX
api: 'api-v1', // API responses
}
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url)
if (url.pathname.match(/\.(png|jpg|webp)$/)) {
event.respondWith(cacheFirst(event.request, CACHES.images))
} else if (url.pathname.match(/\.(mp3|ogg|wav)$/)) {
event.respondWith(cacheFirst(event.request, CACHES.audio))
} else if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(event.request, CACHES.api))
} else {
event.respondWith(cacheFirst(event.request, CACHES.static))
}
})8) Audio/video के लिए range requests
एक बात पहले से जान लें: Cache API partial responses store नहीं करेगा. अगर कोई 206 Partial Content response कभी cache.put() तक पहुंचता है, तो वह TypeError: Failed to execute 'put' on 'Cache': Partial response (status code 206) is unsupported फेंक देता है. इसलिए cached media का pattern हमेशा एक जैसा रहता है: पूरी file को एक सामान्य 200 response के रूप में precache (या fetch) करें, और फिर जब भी कोई request Range header के साथ आए तो cached bytes को slice करके 206 खुद बनाएं. अगर आप इसे हाथ से नहीं लिखना चाहते, तो Workbox का range-requests plugin CacheFirst strategy के साथ मिलकर यही slicing आपके लिए कर देता है.
Range requests को सही तरीके से handle करें (audio seeking के लिए ज़रूरी):
self.addEventListener('fetch', (event) => {
if (event.request.headers.has('range')) {
event.respondWith(handleRangeRequest(event.request))
return
}
// ... normal handling
})
async function handleRangeRequest(request) {
const cache = await caches.open(CACHES.audio)
// Match the full resource, ignoring the Range header (which would never match a cached full file)
let cached = await cache.match(request.url)
if (!cached) {
// Fetch the FULL file (string URL drops the Range header) and cache it as a 200
cached = await fetch(request.url)
if (cached.ok) cache.put(request.url, cached.clone()) // cache.put rejects 206, so only store full 200s
}
// Build a 206 Partial Content response from the cached full body
const rangeHeader = request.headers.get('range') // e.g. "bytes=200-1000"
const buffer = await cached.arrayBuffer()
const total = buffer.byteLength
const [startStr, endStr] = rangeHeader.replace(/bytes=/, '').split('-')
const start = Number(startStr)
const end = endStr ? Number(endStr) : total - 1
const slice = buffer.slice(start, end + 1)
return new Response(slice, {
status: 206,
statusText: 'Partial Content',
headers: {
'Content-Type': cached.headers.get('Content-Type') || 'application/octet-stream',
'Content-Range': `bytes ${start}-${end}/${total}`,
'Content-Length': String(slice.byteLength),
'Accept-Ranges': 'bytes',
},
})
}9) Update notification
Players को बताएं कि नया version तैयार है:
// In main app
let refreshing = false
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (refreshing) return
refreshing = true
showUpdateBanner()
})
function showUpdateBanner() {
const banner = document.createElement('div')
banner.className = 'update-banner'
banner.innerHTML = `
<span>New version available!</span>
<button onclick="location.reload()">Update</button>
`
document.body.appendChild(banner)
}10) पूरा game service worker
const VERSION = 'v1'
const STATIC_CACHE = `static-${VERSION}`
const ASSET_CACHE = `assets-${VERSION}`
const PRECACHE_URLS = [
'/',
'/index.html',
'/game.js',
'/style.css',
]
// Install: precache critical resources
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(STATIC_CACHE)
.then(cache => cache.addAll(PRECACHE_URLS))
.then(() => self.skipWaiting())
)
})
// Activate: clean old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys => Promise.all(
keys
.filter(key => !key.endsWith(VERSION))
.map(key => caches.delete(key))
)).then(() => self.clients.claim())
)
})
// Fetch: route requests to appropriate strategy
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url)
// Same-origin only
if (url.origin !== location.origin) {
return
}
// API: network first with cache fallback
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(event.request, 'api-cache'))
return
}
// Assets: cache first
if (url.pathname.startsWith('/assets/')) {
event.respondWith(cacheFirst(event.request, ASSET_CACHE))
return
}
// HTML/JS/CSS: stale while revalidate
event.respondWith(staleWhileRevalidate(event.request, STATIC_CACHE))
})
async function cacheFirst(request, cacheName) {
const cached = await caches.match(request)
if (cached) return cached
try {
const response = await fetch(request)
if (response.ok) {
const cache = await caches.open(cacheName)
cache.put(request, response.clone())
}
return response
} catch {
return new Response('Offline', { status: 503 })
}
}
async function networkFirst(request, cacheName) {
try {
const response = await fetch(request)
if (response.ok) {
const cache = await caches.open(cacheName)
cache.put(request, response.clone())
}
return response
} catch {
return caches.match(request) || new Response('Offline', { status: 503 })
}
}
async function staleWhileRevalidate(request, cacheName) {
const cache = await caches.open(cacheName)
const cached = await cache.match(request)
const fetchPromise = fetch(request).then(response => {
if (response.ok) {
cache.put(request, response.clone())
}
return response
}).catch(() => cached)
return cached || fetchPromise
}संबंधित
- Offline games के लिए PWA
- Streaming asset loading
- तेज़ी से load होने वाला web game ship करें
- Games के लिए Web Audio API — offline play के लिए audio assets cache करना
- IndexedDB game saves — cached assets के साथ game state persist करना
बाहरी संसाधन
- MDN: Service Worker API — पूरा API reference
- MDN: Cache API — Cache interface reference
- web.dev: Service workers — caching strategies और patterns
- Workbox — service worker tooling के लिए Google की library