Skip to content

PWA pour les jeux web hors ligne

Les Progressive Web Apps permettent aux joueurs d’installer votre jeu et d’y jouer hors ligne. Ce tutoriel explique comment ajouter la prise en charge des PWA à un jeu web.

1) Manifeste d’application web

Créez manifest.json :

json
{
  "name": "Mon jeu génial",
  "short_name": "JeuGénial",
  "description": "Un jeu génial auquel vous pouvez jouer hors ligne",
  "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"
    }
  ]
}

Ajoutez le lien dans votre HTML :

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 de base

Créez sw.js :

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',
]

// Install: cache assets
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(ASSETS)
    })
  )
})

// Activate: clean old caches
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) => {
      return Promise.all(
        keys.filter(key => key !== CACHE_NAME)
            .map(key => caches.delete(key))
      )
    })
  )
})

// Fetch: serve from cache, fall back to network
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      return cached || fetch(event.request)
    })
  )
})

3) Enregistrer le service worker

js
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) Priorité au cache avec mise à jour réseau

Une meilleure approche pour les jeux : chargement rapide et mises à jour en arrière-plan.

js
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.open(CACHE_NAME).then(async (cache) => {
      const cached = await cache.match(event.request)
      
      // Start network fetch in background
      const fetchPromise = fetch(event.request).then((response) => {
        if (response.ok) {
          cache.put(event.request, response.clone())
        }
        return response
      }).catch(() => null)
      
      // Return cached immediately, or wait for network
      return cached || fetchPromise
    })
  )
})

5) Cache versionné pour les mises à jour

js
const CACHE_VERSION = 'v2'
const STATIC_CACHE = `static-${CACHE_VERSION}`
const DYNAMIC_CACHE = `dynamic-${CACHE_VERSION}`

const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/game.js',
  // ... core assets that rarely change
]

self.addEventListener('install', (event) => {
  self.skipWaiting() // Activate immediately
  
  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() // Take control immediately
})

6) Gérer les mises à jour du jeu

Informez les joueurs lorsqu’une mise à jour est disponible :

js
// In main app
let refreshing = false

navigator.serviceWorker.addEventListener('controllerchange', () => {
  if (!refreshing) {
    refreshing = true
    showUpdateNotification()
  }
})

function showUpdateNotification() {
  const banner = document.createElement('div')
  banner.innerHTML = `
    <p>Jeu mis à jour ! Actualisez la page pour obtenir la dernière version.</p>
    <button onclick="location.reload()">Actualiser</button>
  `
  banner.className = 'update-banner'
  document.body.appendChild(banner)
}

7) Détecter le mode hors ligne

js
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) Invite d’installation

js
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
})

Sur iOS et iPadOS, l’événement beforeinstallprompt ne se déclenche jamais. Le bouton d’installation ci-dessus ne s’affiche donc que dans les navigateurs Chromium. Les utilisateurs d’iPhone et d’iPad installent l’application en touchant Partager, puis « Sur l’écran d’accueil ». Un changement intervenu en 2026 mérite d’être signalé : à partir de Safari 26 (iOS 26 / iPadOS 26), chaque site ajouté à l’écran d’accueil s’ouvre par défaut comme une application web, avec une option « Ouvrir comme app web » activée par défaut. Il n’existe plus d’exigence d’installabilité sur iOS, même si fournir un manifeste et un service worker offre toujours une bien meilleure expérience hors ligne.

9) Synchronisation en arrière-plan des classements

Attention à la compatibilité : l’API Background Sync n’est implémentée que dans les navigateurs Chromium (Chrome, Edge, Opera et Samsung Internet). Firefox et Safari, y compris sur iOS, ne la prennent pas en charge. Détectez donc toujours cette fonctionnalité et prévoyez une solution de repli qui envoie les scores au prochain chargement normal de l’application.

js
// In 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) {
      // Will retry on next sync
      break
    }
  }
}

// In main app
async function submitScore(score) {
  try {
    await fetch('/api/scores', { method: 'POST', body: JSON.stringify(score) })
  } catch {
    // Save for later sync
    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) Tester la PWA

Chrome DevTools :

  • Application > Service Workers
  • Application > Manifest
  • Application > Cache Storage
  • Network > Case à cocher Offline

Audit Lighthouse :

  • Exécuter l’audit PWA
  • Vérifier l’installabilité
  • Vérifier le fonctionnement hors ligne

Test sur un appareil réel :

  • Installer le jeu sur l’écran d’accueil du téléphone
  • Activer le mode avion
  • Tester toutes les fonctionnalités hors ligne

À lire aussi

Ressources externes