Chargement progressif des ressources pour les jeux web
Les joueurs n'attendront pas le téléchargement de 100 Mo. Chargez progressivement les ressources afin qu'ils puissent jouer pendant que le reste se charge en arrière-plan.
1) La stratégie de chargement
Répartissez les ressources en plusieurs catégories :
- Critiques — Nécessaires pour afficher le premier écran (< 1 Mo)
- Gameplay — Nécessaires pour jouer (< 10 Mo)
- Améliorations — Facultatives (à charger en arrière-plan)
2) Chargeur de ressources de base
class AssetLoader {
constructor() {
this.cache = new Map()
this.loading = new Map()
}
async loadImage(url) {
if (this.cache.has(url)) return this.cache.get(url)
if (this.loading.has(url)) return this.loading.get(url)
const promise = new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => {
this.cache.set(url, img)
this.loading.delete(url)
resolve(img)
}
img.onerror = reject
img.src = url
})
this.loading.set(url, promise)
return promise
}
async loadJSON(url) {
if (this.cache.has(url)) return this.cache.get(url)
const response = await fetch(url)
const data = await response.json()
this.cache.set(url, data)
return data
}
async loadAudio(url, audioCtx) {
if (this.cache.has(url)) return this.cache.get(url)
const response = await fetch(url)
const buffer = await response.arrayBuffer()
const audioBuffer = await audioCtx.decodeAudioData(buffer)
this.cache.set(url, audioBuffer)
return audioBuffer
}
}3) Chargement avec suivi de la progression
async function loadWithProgress(url, onProgress) {
const response = await fetch(url)
const contentLength = response.headers.get('Content-Length')
const total = parseInt(contentLength, 10)
const reader = response.body.getReader()
const chunks = []
let received = 0
while (true) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value)
received += value.length
onProgress(received / total)
}
const blob = new Blob(chunks)
return blob
}Attention : lorsque le serveur compresse la réponse (avec gzip ou brotli, comme le font la plupart des CDN par défaut), Content-Length correspond à la taille compressée, tandis que les fragments lus sont déjà décompressés. Le rapport received / total dépasse alors 100 %. Pour obtenir une barre de progression précise au niveau des octets avec les réponses compressées, servez la ressource sans compression, envoyez votre propre en-tête indiquant la taille décompressée ou utilisez l'événement onprogress de XMLHttpRequest, qui indique la progression réelle du transfert.
4) Chargement par lots avec progression globale
async function loadAssets(manifest, onProgress) {
const total = manifest.length
let completed = 0
const results = {}
const promises = manifest.map(async (item) => {
const asset = await loadAsset(item.url, item.type)
results[item.name] = asset
completed++
onProgress(completed / total, item.name)
})
await Promise.all(promises)
return results
}
// Utilisation
const manifest = [
{ name: 'player', url: 'player.png', type: 'image' },
{ name: 'level1', url: 'level1.json', type: 'json' },
{ name: 'music', url: 'music.mp3', type: 'audio' },
]
const assets = await loadAssets(manifest, (progress, name) => {
console.log(`Chargement : ${Math.round(progress * 100)}% (${name})`)
})5) Chargeur avec file d'attente prioritaire
class PriorityLoader {
constructor(concurrency = 4) {
this.queue = []
this.active = 0
this.concurrency = concurrency
}
add(url, priority = 0) {
return new Promise((resolve, reject) => {
this.queue.push({ url, priority, resolve, reject })
this.queue.sort((a, b) => b.priority - a.priority)
this.process()
})
}
async process() {
if (this.active >= this.concurrency || this.queue.length === 0) return
this.active++
const { url, resolve, reject } = this.queue.shift()
try {
const response = await fetch(url)
const blob = await response.blob()
resolve(blob)
} catch (err) {
reject(err)
}
this.active--
this.process()
}
}
// Utilisation
const loader = new PriorityLoader()
loader.add('critical.png', 10) // Charger en premier
loader.add('optional.png', 1) // Charger plus tard6) Chargement différé des niveaux
class LevelManager {
constructor(loader) {
this.loader = loader
this.levels = new Map()
}
async preload(levelId) {
if (this.levels.has(levelId)) return
const manifest = await this.loader.loadJSON(`levels/${levelId}/manifest.json`)
const assets = await this.loadLevelAssets(manifest)
this.levels.set(levelId, { manifest, assets })
}
async loadLevelAssets(manifest) {
// Charger uniquement ce qui est nécessaire pour ce niveau
const assets = {}
for (const texture of manifest.textures) {
assets[texture.name] = await this.loader.loadImage(texture.url)
}
return assets
}
unload(levelId) {
this.levels.delete(levelId)
// Les ressources peuvent être récupérées par le ramasse-miettes
}
}7) Streaming de fichiers volumineux
Pour les fichiers volumineux (modèles 3D, audio), utilisez le streaming et traitez-les progressivement :
async function streamLargeFile(url, onChunk) {
const response = await fetch(url)
const reader = response.body.getReader()
while (true) {
const { done, value } = await reader.read()
if (done) break
onChunk(value)
}
}
// Pour l'audio : utiliser Media Source Extensions
// Pour la 3D : traiter les données du maillage à mesure qu'elles arrivent8) Mise en cache des ressources chargées
Associez le chargeur à IndexedDB pour une mise en cache persistante :
class CachedLoader {
constructor() {
this.memCache = new Map()
this.dbName = 'AssetCache'
}
async loadImage(url) {
// Vérifier la mémoire
if (this.memCache.has(url)) return this.memCache.get(url)
// Vérifier IndexedDB
const cached = await this.getFromDB(url)
if (cached) {
const img = await this.blobToImage(cached)
this.memCache.set(url, img)
return img
}
// Récupérer et mettre en cache
const response = await fetch(url)
const blob = await response.blob()
await this.saveToDB(url, blob)
const img = await this.blobToImage(blob)
this.memCache.set(url, img)
return img
}
blobToImage(blob) {
return new Promise((resolve) => {
const img = new Image()
img.onload = () => {
URL.revokeObjectURL(img.src)
resolve(img)
}
img.src = URL.createObjectURL(blob)
})
}
// Méthodes IndexedDB...
}9) Modèle d'écran de chargement
class LoadingScreen {
constructor(canvas) {
this.canvas = canvas
this.ctx = canvas.getContext('2d')
this.progress = 0
this.message = 'Chargement...'
}
update(progress, message) {
this.progress = progress
this.message = message || this.message
this.render()
}
render() {
const { ctx, canvas } = this
ctx.fillStyle = '#1a1a2e'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Barre de progression
const barWidth = canvas.width * 0.6
const barHeight = 20
const x = (canvas.width - barWidth) / 2
const y = canvas.height / 2
ctx.fillStyle = '#333'
ctx.fillRect(x, y, barWidth, barHeight)
ctx.fillStyle = '#4ade80'
ctx.fillRect(x, y, barWidth * this.progress, barHeight)
// Texte
ctx.fillStyle = '#fff'
ctx.font = '16px sans-serif'
ctx.textAlign = 'center'
ctx.fillText(this.message, canvas.width / 2, y - 20)
ctx.fillText(`${Math.round(this.progress * 100)}%`, canvas.width / 2, y + 50)
}
}10) Bonnes pratiques
- Affichez immédiatement quelque chose — même une image statique
- Chargez d'abord ce qui est visible — les textures de la vue actuelle
- Utilisez des espaces réservés — des images en basse résolution remplacées ensuite
- Préchargez le niveau suivant — pendant que le joueur joue encore
- Gérez les échecs sans interrompre l'expérience — nouvelles tentatives et solutions de repli
async function loadWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fetch(url)
} catch (err) {
if (i === maxRetries - 1) throw err
await new Promise(r => setTimeout(r, 1000 * (i + 1)))
}
}
}Articles connexes
- Publier un jeu web qui se charge rapidement
- Service workers pour la mise en cache des jeux
- Sauvegardes de jeu avec IndexedDB
- Web Workers pour la logique de jeu — décodez et traitez les ressources en dehors du thread principal
- Où trouver des ressources de jeu gratuites — sources de modèles 3D, de textures et de contenus audio à diffuser en streaming
Ressources externes
- MDN : API Fetch — réponses en streaming et ReadableStream
- MDN : ReadableStream — traitement des fragments de données diffusés en streaming
- Spécification glTF 2.0 — le format standard pour la diffusion en streaming de ressources 3D