Optimiser les performances WebGL : draw calls, instancing, LOD et textures
Une scène Three.js qui tourne à 60 fps en développement sur un MacBook Pro M3 peut tomber à 15 fps sur le smartphone mid-range de votre utilisateur. WebGL est puissant mais impitoyable : chaque draw call, chaque texture non compressée, chaque shader non optimisé se paye en latence. Ce guide couvre les techniques d'optimisation qui font réellement la différence en production : réduction des draw calls, LOD, compression de textures, instancing, frustum culling et profiling GPU.
Profiler d'abord, optimiser ensuite
Avant d'optimiser quoi que ce soit, mesurez. Three.js expose ses statistiques via la librairiestats.js: FPS, MS par frame, MB de mémoire. Pour une analyse plus fine du goulot d'étranglement, l'onglet Performancede Chrome DevTools permet d'enregistrer une session GPU et de distinguer les frames CPU-bound (code JavaScript trop lent) des frames GPU-bound (trop de géométrie ou de fragments à traiter).
import Stats from 'stats.js';
const stats = new Stats();
stats.showPanel(0); // 0: fps, 1: ms, 2: mb
document.body.appendChild(stats.dom);
function animate() {
stats.begin();
// ... rendu
stats.end();
requestAnimationFrame(animate);
}L'extension Chrome Spector.jsest l'outil de référence pour inspecter les draw calls WebGL : elle capture un frame et liste chaque appel WebGL avec son coût GPU, les textures et les shaders impliqués. Indispensable pour identifier les draw calls redondants ou les textures non optimisées.
Réduire les draw calls : la règle numéro un
Chaque appel renderer.render() → gl.drawElements() est un draw call. Le CPU doit préparer les données, les envoyer au GPU, puis le GPU les traite. Le temps de préparation CPU est fixe par draw call — indépendamment du nombre de triangles. Avoir 1 000 draw calls de 10 triangles chacun est bien plus lent que 1 draw call de 10 000 triangles.
Instanced Meshest la solution pour rendre des milliers de copies d'un même objet avec un seul draw call. Au lieu de créer 1 000 Mesh identiques, on crée unInstancedMesh avec une capacité maximale et on définit la transformation de chaque instance via une matrice :
const count = 5000;
const geometry = new THREE.SphereGeometry(0.05, 8, 8);
const material = new THREE.MeshStandardMaterial({ color: 0xC026D3 });
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);
const matrix = new THREE.Matrix4();
const position = new THREE.Vector3();
for (let i = 0; i < count; i++) {
position.set(
(Math.random() - 0.5) * 20,
(Math.random() - 0.5) * 20,
(Math.random() - 0.5) * 20
);
matrix.setPosition(position);
instancedMesh.setMatrixAt(i, matrix);
}
instancedMesh.instanceMatrix.needsUpdate = true;
scene.add(instancedMesh);5 000 sphères en 1 draw call au lieu de 5 000. Le gain de performance est typiquement de 10x à 100x selon le GPU et le CPU.
Fusionner les géométries avec BufferGeometryUtils
Pour les objets statiques qui n'ont pas besoin de transformations individuelles (décors, terrain, détails environnementaux), fusionner les géométries en une seule viaTHREE.BufferGeometryUtils.mergeGeometries()produit un draw call unique. La contrepartie est qu'on perd la possibilité de déplacer ou masquer les géométries fusionnées individuellement — acceptable pour les éléments de décor statiques.
Level of Detail (LOD) : adapter la complexité à la distance
Un modèle 3D à 50 000 polygones rendu à 2 pixels de large est un gaspillage pur. Le LOD (Level of Detail) résout ce problème en substituant automatiquement une version simplifiée du modèle quand l'objet est loin de la caméra.
const lod = new THREE.LOD();
// Version haute résolution (proche)
const highGeo = new THREE.SphereGeometry(1, 32, 32);
lod.addLevel(new THREE.Mesh(highGeo, material), 0);
// Version moyenne (distance intermédiaire)
const medGeo = new THREE.SphereGeometry(1, 16, 16);
lod.addLevel(new THREE.Mesh(medGeo, material), 10);
// Version basse résolution (loin)
const lowGeo = new THREE.SphereGeometry(1, 6, 6);
lod.addLevel(new THREE.Mesh(lowGeo, material), 30);
scene.add(lod);
// lod.update(camera) doit être appelé dans la boucle d'animationTextures : compression et mipmap
Les textures représentent souvent 80 % de la mémoire GPU et du temps de chargement d'une scène. Deux optimisations incontournables : la compression GPU et lesmipmaps. Les formats de compression GPU (KTX2 avec UASTC/ETC1S) réduisent la taille en mémoire GPU de 4x à 8x par rapport au PNG/JPEG décompressé, avec une qualité visuelle quasi identique. Three.js supporte KTX2 via KTX2Loader et le transcoder Basis Universal.
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
const ktx2Loader = new KTX2Loader()
.setTranscoderPath('/basis/')
.detectSupport(renderer);
const texture = await ktx2Loader.loadAsync('/textures/albedo.ktx2');
texture.generateMipmaps = true; // automatique pour KTX2Les mipmapssont des versions pré-calculées de la texture à différentes résolutions (512, 256, 128...). Quand l'objet est loin, le GPU utilise la version basse résolution, évitant les artefacts de moiré et réduisant le nombre de texels à échantillonner. Three.js génère les mipmaps automatiquement au chargement — vérifiez que texture.generateMipmaps = true(valeur par défaut pour les textures non-compressées).
Frustum culling et occlusion culling
Three.js effectue automatiquement le frustum culling: les objets en dehors du champ de vision de la caméra ne sont pas envoyés au GPU. Pour les scènes avec beaucoup d'objets, activer mesh.frustumCulled = true(valeur par défaut) est suffisant pour les objets volumineux. Pour des objets très petits ou des scènes de type couloir (FPS, visite virtuelle), une implémentation d'occlusion culling via des queries WebGL ou une octree spatiale peut réduire considérablement le nombre de draw calls.
Web Workers pour les calculs lourds
Les calculs CPU lourds — génération procédurale de géométrie, pathfinding, physique — bloquent le thread principal et font tomber le framerate, même si le GPU est rapide. Les Web Workers exécutent ces calculs sur un thread séparé. Three.js peut transférer des BufferGeometryvers un Worker via geometry.toJSON() ou les transferables ArrayBuffer. Pour les effets de particules complexes calculés sur GPU, les GPGPU (General Purpose GPU) via GPUComputationRendererde Three.js ou via WebGPU sont l'étape suivante vers des simulations de millions de particules en temps réel — une spécialité que nous explorons dans nos projets d'expériences Three.js immersives.
“60 fps n'est pas un objectif de luxe — c'est le seuil en dessous duquel votre expérience immersive devient une expérience frustrante. L'optimisation WebGL est autant du design que de la performance.”
Optimiser une scène WebGL est un travail de mesure, d'hypothèse et de validation continue. Il n'existe pas de recette universelle — chaque scène a son propre goulot d'étranglement. Ce que nous savons, c'est que les sites web immersifs que nous développons depuis Brive-la-Gaillarde pour nos clients en Nouvelle-Aquitaine et au-delà doivent tourner parfaitement sur l'ensemble des appareils de leurs utilisateurs. Pour concevoir une expérience WebGL performante et mémorable, découvrez notre expertise Web Immersif ou prenez contact.