ITSkillsCenter
Business Digital

Activer WebGPU en React Three Fiber avec fallback WebGL 2

14 دقائق للقراءة

📍 Article principal de la série : Three.js, React Three Fiber et WebGPU en 2026 : 3D temps réel sur le web
Ce tutoriel suppose que vous avez déjà un projet R3F v9 fonctionnel. Si ce n’est pas le cas, lire d’abord Démarrer React Three Fiber v9 avec Vite et TypeScript.

Introduction

WebGPU est désormais disponible dans Chrome, Edge, Firefox et Safari 26. Activer le WebGPURenderer dans une application React Three Fiber n’est pas trivial : l’initialisation est asynchrone et certains navigateurs anciens n’ont pas l’API. Ce tutoriel montre comment instancier proprement un WebGPURenderer dans R3F v9, comment détecter la disponibilité de WebGPU, et comment retomber automatiquement sur WebGL 2 quand WebGPU n’est pas disponible. À la fin, votre application tourne sur la GPU moderne là où c’est possible et ne perd aucun utilisateur sur les plateformes plus anciennes.

Prérequis

  • Un projet R3F v9 fonctionnel (cf. tutoriel précédent)
  • Three.js r171 ou supérieur (r184 recommandé)
  • Un navigateur récent pour tester : Chrome 122+, Edge 122+, Firefox 121+, Safari 26+
  • Niveau intermédiaire en React (compréhension de useState, useEffect, des promesses)
  • Temps estimé : 30 à 40 minutes

Étape 1 — Comprendre l’initialisation asynchrone de WebGPU

Avant d’écrire la moindre ligne, il faut comprendre pourquoi le WebGPURenderer ne peut pas être instancié de façon synchrone comme le WebGLRenderer. L’API WebGPU expose navigator.gpu, qui ne devient utilisable qu’après deux étapes asynchrones successives. La première est l’obtention d’un adapter, qui représente une carte graphique disponible côté machine. La seconde est l’obtention d’un device, qui est la session logique sur cet adapter. Chaque étape est une promesse, et le processus peut échouer si la GPU est indisponible ou si le navigateur refuse l’accès pour des raisons de sécurité.

// Schéma d'initialisation WebGPU brut, pour la pédagogie
const adapter = await navigator.gpu.requestAdapter()
const device = await adapter.requestDevice()
// À ce stade seulement on peut créer un WebGPURenderer Three.js

Le WebGPURenderer de Three.js encapsule ces deux étapes derrière une méthode renderer.init() qui retourne une promesse. Tant que cette promesse n’est pas résolue, on ne peut rien rendre. C’est précisément pour gérer cette contrainte que React Three Fiber v9 a introduit le support d’une fonction gl asynchrone : on lui passe une fonction qui retourne la promesse du renderer initialisé, et R3F attend la résolution avant de monter la scène.

Étape 2 — Détecter la disponibilité de WebGPU

Avant de tenter une initialisation, on vérifie que le navigateur expose bien navigator.gpu. C’est un simple test JavaScript qui ne déclenche aucune allocation GPU et permet de décider, dès le démarrage, si on prend la voie WebGPU ou la voie WebGL 2. On crée un fichier utilitaire src/utils/gpu.ts.

// src/utils/gpu.ts
export function hasWebGPU(): boolean {
  return typeof navigator !== 'undefined' && 'gpu' in navigator
}

export async function probeWebGPU(): Promise<boolean> {
  if (!hasWebGPU()) return false
  try {
    const adapter = await (navigator as any).gpu.requestAdapter()
    return adapter !== null
  } catch {
    return false
  }
}

La fonction hasWebGPU retourne immédiatement true ou false selon la présence de l’API. probeWebGPU va plus loin : elle tente l’obtention d’un adapter, ce qui révèle qu’un navigateur peut exposer l’API mais ne pas avoir de GPU compatible — cas qu’on rencontre sur certaines machines virtuelles ou certaines plateformes Linux mal configurées. Le test est silencieux et rapide ; il alloue une milliseconde au démarrage.

Étape 3 — Importer le WebGPURenderer Three.js

Le WebGPURenderer ne fait pas partie de l’export racine de Three.js ; il vit dans un sous-chemin pour ne pas alourdir les bundles qui n’en ont pas besoin. Depuis r171, l’import est stable et ne nécessite plus de configuration bundler particulière, contrairement aux versions précédentes où il fallait régler des alias dans Vite.

// src/renderer/createWebGPURenderer.ts
import * as THREE from 'three/webgpu'

export async function createWebGPURenderer(canvas: HTMLCanvasElement) {
  const renderer = new THREE.WebGPURenderer({
    canvas,
    antialias: true,
    powerPreference: 'high-performance',
  })
  await renderer.init()
  return renderer
}

L’import three/webgpu est documenté dans le package.json exports de Three.js. Il pointe vers une version étendue de la bibliothèque qui inclut le WebGPURenderer ainsi que tous les nœuds TSL associés. La fonction createWebGPURenderer instancie le renderer puis attend l’initialisation. Si renderer.init() rejette, c’est qu’aucun adapter compatible n’a pu être obtenu — l’erreur remonte alors dans le contexte appelant.

Étape 4 — Brancher le WebGPURenderer sur le Canvas R3F

Le composant <Canvas> de R3F v9 accepte une fonction gl qui reçoit le canvas DOM et retourne soit un renderer, soit une promesse de renderer. C’est le point d’attache pour notre fonction asynchrone.

// src/scenes/SceneWebGPU.tsx
import { Canvas } from '@react-three/fiber'
import { createWebGPURenderer } from '@/renderer/createWebGPURenderer'

export default function SceneWebGPU() {
  return (
    <Canvas
      gl={async (canvas) => createWebGPURenderer(canvas as HTMLCanvasElement)}
      camera={{ position: [3, 2, 3], fov: 50 }}
    >
      <ambientLight intensity={0.3} />
      <directionalLight position={[5, 5, 5]} intensity={1.2} />
      <mesh>
        <boxGeometry args={[1, 1, 1]} />
        <meshStandardMaterial color="#ff6b00" />
      </mesh>
    </Canvas>
  )
}

R3F détecte que la prop gl est une fonction async, attend sa résolution, puis monte la scène. Avant la résolution, le Canvas reste vide ; c’est une fenêtre courte (typiquement moins de 50 ms) qu’on peut combler avec un fallback visuel si nécessaire. Si on lance npm run dev et qu’on ouvre la page sur Chrome 122+ ou Safari 26+, la scène s’affiche en passant par la GPU moderne. On le confirme via les DevTools : l’onglet Sources expose un objet WebGPURenderer dans la pile de Three.js.

Étape 5 — Implémenter le repli automatique vers WebGL 2

Si navigator.gpu n’est pas disponible ou si l’initialisation échoue, on doit basculer transparentement sur le WebGLRenderer historique. Le pattern propre est de centraliser la décision dans une fonction createRenderer qui essaie WebGPU et retombe sur WebGL en cas d’échec.

// src/renderer/createRenderer.ts
import * as THREE from 'three'
import * as THREE_WEBGPU from 'three/webgpu'
import { hasWebGPU } from '@/utils/gpu'

export type RendererKind = 'webgpu' | 'webgl'
export type CreatedRenderer = {
  renderer: THREE.WebGLRenderer | THREE_WEBGPU.WebGPURenderer
  kind: RendererKind
}

export async function createRenderer(
  canvas: HTMLCanvasElement,
): Promise<CreatedRenderer> {
  if (hasWebGPU()) {
    try {
      const renderer = new THREE_WEBGPU.WebGPURenderer({
        canvas,
        antialias: true,
        powerPreference: 'high-performance',
      })
      await renderer.init()
      return { renderer, kind: 'webgpu' }
    } catch (err) {
      console.warn('[renderer] WebGPU init failed, falling back to WebGL', err)
    }
  }
  const fallback = new THREE.WebGLRenderer({ canvas, antialias: true })
  return { renderer: fallback, kind: 'webgl' }
}

La fonction tente d’abord WebGPU. Si hasWebGPU retourne false — typiquement Chrome avant 113 ou Safari avant 26 — on passe directement à WebGL. Si l’API est exposée mais que renderer.init() échoue, on log un avertissement et on retombe aussi sur WebGL. Le retour inclut le kind, ce qui permettra plus tard d’adapter certains comportements (par exemple désactiver certains effets de post-processing qui exigent des compute shaders).

Étape 6 — Exposer le kind du renderer dans le contexte R3F

R3F v9 expose le renderer via useThree((state) => state.gl) — la propriété s’appelle toujours gl en v9 stable. Le renommage en state.renderer est annoncé pour la v10 et n’arrive qu’avec cette branche, encore en alpha au moment de la rédaction. Pour savoir si on tourne en WebGPU ou WebGL, on inspecte le constructeur du renderer plutôt que de transporter un état dupliqué.

// src/hooks/useRendererKind.ts
import { useThree } from '@react-three/fiber'

export function useRendererKind(): 'webgpu' | 'webgl' {
  const renderer = useThree((s) => s.gl) // v10 alpha : remplacer par s.renderer
  const name = renderer?.constructor?.name ?? ''
  return name.includes('WebGPU') ? 'webgpu' : 'webgl'
}

Ce hook est une pure observation — il ne déclenche pas de re-render si le renderer ne change pas. On l’utilise dans les composants enfants pour adapter conditionnellement leur comportement, par exemple charger une chaîne de post-processing TSL en WebGPU et une chaîne pmndrs/postprocessing en WebGL.

Étape 7 — Afficher l’information à l’utilisateur en mode dev

Pendant le développement, savoir d’un coup d’œil quel renderer est actif aide à diagnostiquer les bugs. On ajoute un overlay simple qui affiche le kind, désactivé en production via import.meta.env.DEV.

// src/components/RendererBadge.tsx
import { useRendererKind } from '@/hooks/useRendererKind'

export default function RendererBadge() {
  const kind = useRendererKind()
  if (!import.meta.env.DEV) return null
  return (
    <div style={{
      position: 'absolute', top: 8, right: 8,
      padding: '4px 8px', borderRadius: 4,
      background: kind === 'webgpu' ? '#0a7' : '#a70',
      color: 'white', fontFamily: 'monospace', fontSize: 12, zIndex: 10,
    }}>
      {kind.toUpperCase()}
    </div>
  )
}

Le composant doit être placé en dehors du Canvas, au niveau du parent, parce qu’il s’agit d’un élément DOM et non d’un objet 3D. Le badge passe en vert quand WebGPU est actif et en orange en repli WebGL. C’est un signal visuel précieux quand on teste sur plusieurs navigateurs successifs.

Étape 8 — Tester le repli en simulant l’absence de WebGPU

Pour vérifier que le fallback fonctionne sans avoir à changer de navigateur, on peut neutraliser temporairement navigator.gpu dans la console de Chrome avant de recharger la page. C’est la méthode la plus rapide pour tester les deux branches du code en une session de développement.

// À coller dans la console DevTools avant rechargement
Object.defineProperty(navigator, 'gpu', { get: () => undefined })
location.reload()

Après rechargement, le badge doit afficher WEBGL en orange. Une commande équivalente sous Firefox passe par about:config en désactivant dom.webgpu.enabled. La scène doit s’afficher identiquement à l’œil ; seul le chemin GPU sous-jacent change. Si on observe une différence visuelle entre les deux modes — couleurs déformées, ombres absentes — c’est un bug dans la scène, pas dans le repli, et il faut l’analyser avant publication.

Étape 9 — Gérer le cas où WebGPU est disponible mais la scène utilise un nœud non transpilable

Certains nœuds TSL utilisent des compute shaders ou des fonctionnalités WGSL qui n’ont pas d’équivalent en GLSL ES 3.0. Si on les inclut sans précaution, la scène fonctionne en WebGPU mais retourne une erreur au moment du repli WebGL. La règle est de tester les chemins critiques sur les deux backends pendant le développement, et d’isoler les fonctionnalités exigeantes derrière une condition sur useRendererKind.

function ConditionalEffect() {
  const kind = useRendererKind()
  if (kind !== 'webgpu') return null
  // Effet exclusivement WebGPU, par exemple un compute shader
  return <ComputeOnlyEffect />
}

Ce pattern est plus prudent que d’ajouter des branches dans chaque shader. Il garde la complexité en surface, lisible, et facilite la maintenance. Quand WebGPU sera totalement universel — vraisemblablement d’ici 2027 selon le rythme actuel d’adoption — il suffira de retirer ces branches sans toucher au cœur des effets.

Étape 10 — Mesurer la performance différentielle des deux renderers

Décider entre WebGPU et WebGL ne doit pas être un acte de foi : on mesure. Le helper <Stats /> de Drei donne le FPS instantané, mais pour un benchmark sérieux il vaut mieux instrumenter une mesure du temps de frame moyen sur quelques secondes. On crée un hook simple qui collecte les valeurs et les affiche en console au bout de cinq secondes, ce qui permet de comparer objectivement les deux backends sur la même scène.

// src/hooks/useFrameBenchmark.ts
import { useFrame } from '@react-three/fiber'
import { useRef } from 'react'

export function useFrameBenchmark(label: string) {
  const samples = useRef<number[]>([])
  const start = useRef(performance.now())

  useFrame((_, delta) => {
    samples.current.push(delta * 1000) // delta en ms
    if (performance.now() - start.current > 5000) {
      const arr = samples.current
      const avg = arr.reduce((a, b) => a + b, 0) / arr.length
      const sorted = [...arr].sort((a, b) => a - b)
      const p95 = sorted[Math.floor(sorted.length * 0.95)]
      console.log(`[bench:${label}] avg=${avg.toFixed(2)}ms p95=${p95.toFixed(2)}ms`)
      samples.current = []
      start.current = performance.now()
    }
  })
}

On appelle ce hook dans un composant enfant du Canvas. La sortie console affiche, toutes les cinq secondes, la moyenne et le 95e percentile du temps de frame en millisecondes. Sur une scène donnée, on prend une mesure en WebGPU, puis on force le repli WebGL avec la commande de l’étape 8 et on prend une seconde mesure. La différence est immédiatement parlante : si WebGPU est nettement plus rapide, on confirme le choix ; si l’écart est nul ou défavorable, c’est typiquement le signe d’une scène trop simple pour bénéficier des compute shaders et on peut envisager de rester en WebGL pour réduire la surface d’attaque bug. Cette mesure différentielle, faite sur la cible de production réelle, vaut mieux que n’importe quelle recommandation générique.

Erreurs fréquentes

Erreur Cause Solution
Cannot find module 'three/webgpu' Three.js antérieur à r171 Mettre à jour three à la dernière version stable
Canvas blanc qui reste vide renderer.init() a rejeté silencieusement Encadrer le await par un try/catch et logger l’erreur
FPS chute drastiquement après bascule WebGPU Une feature WebGL était plus performante sur la machine de test Bench les deux ; pour les machines anciennes, WebGL peut rester préférable
Repli ne se déclenche pas hasWebGPU() retourne true mais l’init échoue plus tard Toujours try/catch autour de renderer.init()
Erreur compute shader en WebGL Nœud TSL non transpilable utilisé sans condition Utiliser useRendererKind pour conditionner

Tutoriels frères

Pour étoffer le tableau

FAQ

Quelle différence concrète entre WebGPU et WebGL 2 dans une scène simple ?
Pour une scène à dix mille triangles sans compute shader, l’œil ne voit aucune différence. WebGPU prend l’avantage dès qu’on charge la GPU sérieusement : particules, fluides, instancing massif, post-processing à plusieurs passes. C’est donc une valeur sûre pour les nouveaux projets, mais pas une raison de réécrire un projet WebGL 2 stable.

Faut-il forcer WebGPU même sur des navigateurs anciens ?
Non. La logique de repli automatique est précisément faite pour éviter ce piège. Sur un Chrome 110, par exemple, l’API n’existe pas et on retombe naturellement sur WebGL.

Le badge dev affiche WEBGL alors que je suis sur Chrome 124. Pourquoi ?
Trois causes possibles. Premièrement, une extension de navigateur (Privacy Badger, certaines extensions enterprise) bloque l’accès à navigator.gpu. Deuxièmement, la machine n’a pas de GPU compatible — typique sur certaines VM. Troisièmement, le flag d’activation a été désactivé dans les paramètres avancés.

Peut-on changer de renderer à chaud sans recharger la page ?
Techniquement oui, mais c’est complexe : il faut démonter complètement la scène, libérer les ressources GPU, monter un nouveau Canvas. En pratique, on prend la décision au démarrage et on la conserve pour la session.

WebGPU consomme-t-il plus de batterie ?
À charge égale, plutôt moins, parce que l’API permet une gestion plus fine du sleep GPU. Mais une scène WebGPU profite souvent de la disponibilité pour faire plus de choses (compute, particules) — la consommation finale dépend donc du contenu, pas de l’API.

Comment vérifier qu’il n’y a pas de fuite GPU au montage/démontage du Canvas ?
Sous Chrome, ouvrir chrome://gpu avant et après plusieurs cycles de navigation pour observer la consommation mémoire. Sous Safari, l’inspecteur expose une section GPU. Une fuite typique est un renderer ou une geometry non disposée au démontage du composant. La règle systématique est d’appeler renderer.dispose() et scene.traverse(o => { o.geometry?.dispose(); o.material?.dispose() }) dans le cleanup du useEffect racine ou via le pattern onCreated du Canvas. R3F gère bien la majorité des cas, mais le custom gl async demande un soin particulier puisque c’est nous qui avons créé l’instance.

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité