almessadi.
Retour à l'index

Quand WebGL surpasse le DOM pour le rendu d'interfaces utilisateur complexes_

Comment reconnaître quand le DOM, SVG ou Canvas cessent de se redimensionner, et quand WebGL devient le meilleur chemin de rendu pour des interfaces complexes et riches en données.

Publié4 mars 2024
Temps de lecture24 min read

La plupart des problèmes de performance frontend ne nécessitent pas WebGL.

Si vous animez une pile de cartes, un tooltip ou même un graphique modérément chargé, le DOM est généralement la bonne abstraction. Il est plus facile à déboguer, plus facile à rendre accessible et plus facile à intégrer avec le reste de votre produit.

WebGL devient intéressant lorsque le navigateur passe plus de temps à gérer des milliers de primitives visuelles que votre application à exécuter la logique réelle du produit. Cela se manifeste généralement par une combinaison de ces symptômes :

  • Pics du temps de trame lors de la recalculation du style ou du rendu
  • Les nœuds SVG ou les appels de dessin du canvas atteignent des dizaines de milliers
  • Les interactions deviennent sensiblement plus difficiles sur des ordinateurs portables ordinaires, pas seulement sur votre machine de développement
  • L'interface utilisateur est visuellement simple, mais la charge de rendu est importante

C'est à ce moment-là que "optimiser le composant React" cesse d'être un plan sérieux.

Qu'est-ce qui change quand vous passez à WebGL

Avec le rendu DOM ou SVG, le navigateur s'occupe de beaucoup de travail pour vous :

  • mise en page
  • résolution de style
  • test de collision
  • peinture
  • composition

Cette commodité a un coût. Chaque élément participe à des pipelines du navigateur qui ont été conçus pour des documents en premier et des graphiques en second.

WebGL prend le compromis opposé. Vous téléchargez la géométrie et les données par instance dans des tampons GPU, écrivez des shaders pour indiquer au GPU comment dessiner, et maintenez le navigateur hors du chemin critique. Vous perdez en commodité, mais vous gagnez en contrôle sur le débit.

Pour des scènes denses et animées, cet échange peut en valoir la peine.

Le problème de performance que vous résolvez réellement

L'erreur courante est de penser que "WebGL est plus rapide" en règle générale. Ce n'est pas le cas.

WebGL est plus rapide lorsque :

  • la scène contient de nombreux objets visuels similaires
  • ces objets peuvent être représentés sous forme de données numériques structurées
  • le travail consiste principalement en dessin, interpolation, transformations ou calculs de couleurs

WebGL est souvent le mauvais outil lorsque :

  • la scène est principalement textuelle
  • les sémantiques d'accessibilité ont de l'importance au niveau des éléments
  • vous avez besoin d'un comportement de mise en page natif du navigateur
  • la complexité réside dans la récupération de données ou le changement d'état, pas le rendu

Un bon exemple est une vue de télémétrie avec des milliers de points en mouvement. Un mauvais exemple est une page marketing avec six blobs décoratifs.

Une configuration WebGL minimale

La première étape consiste simplement à obtenir une surface de rendu prédictible :

const canvas = document.querySelector("canvas");
const gl = canvas?.getContext("webgl2");

if (!gl) {
  throw new Error("WebGL2 n'est pas disponible dans ce navigateur");
}

function resizeCanvas() {
  const dpr = window.devicePixelRatio || 1;
  const width = Math.floor(canvas.clientWidth * dpr);
  const height = Math.floor(canvas.clientHeight * dpr);

  if (canvas.width !== width || canvas.height !== height) {
    canvas.width = width;
    canvas.height = height;
    gl.viewport(0, 0, width, height);
  }
}

resizeCanvas();
window.addEventListener("resize", resizeCanvas);

Deux détails sont importants ici :

  • Vous dimensionnez le tampon de soutien avec devicePixelRatio, pas seulement en pixels CSS.
  • Vous appelez gl.viewport(...) après le redimensionnement, sinon votre scène sera rendue avec des dimensions périmées.

Le véritable avantage : garder les données sur le GPU

L'amélioration de performance la plus significative en WebGL ne provient généralement pas d'un shader astucieux. Elle provient de l'évitement d'un trafic inutile CPU-GPU.

Voici le type de chemin de mise à jour qui tue discrètement la performance :

function renderFrame(points: Float32Array) {
  const buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
  gl.drawArrays(gl.POINTS, 0, points.length / 2);
}

Cela réalloue des ressources sur le chemin critique. C'est l'équivalent GPU de la reconstruction du monde à chaque image.

Un modèle plus sain consiste à allouer une fois et à mettre à jour sur place :

const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, MAX_POINTS * 2 * Float32Array.BYTES_PER_ELEMENT, gl.DYNAMIC_DRAW);

function updatePoints(points: Float32Array) {
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.bufferSubData(gl.ARRAY_BUFFER, 0, points);
}

Si le nombre de points est borné, la préallocation est généralement le bon comportement par défaut.

Les shaders ne sont que des programmes

WebGL semble intimidant jusqu'à ce que vous cessiez de considérer les shaders comme de la "magie graphique" et commenciez à les penser comme de petits programmes avec des entrées et des sorties strictes.

Un shader de sommet détermine où quelque chose atterrit à l'écran :

#version 300 es
in vec2 a_position;
uniform vec2 u_resolution;

void main() {
  vec2 zeroToOne = a_position / u_resolution;
  vec2 zeroToTwo = zeroToOne * 2.0;
  vec2 clipSpace = zeroToTwo - 1.0;

  gl_Position = vec4(clipSpace * vec2(1.0, -1.0), 0.0, 1.0);
  gl_PointSize = 4.0;
}

Un shader de fragment détermine comment les pixels résultants apparaissent :

#version 300 es
precision highp float;

out vec4 outColor;

void main() {
  vec2 p = gl_PointCoord - vec2(0.5);
  float distanceFromCenter = length(p);
  float alpha = smoothstep(0.5, 0.0, distanceFromCenter);

  outColor = vec4(0.14, 0.72, 0.97, alpha);
}

Cela suffit pour rendre des points circulaires doux sans demander au DOM de gérer des milliers d'éléments.

Où React trouve encore sa place

Déplacer le rendu vers WebGL ne signifie pas enlever React de l'application.

La séparation claire est :

  • React gère les contrôles, filtres, mise en page et chargement des données
  • WebGL gère la surface de dessin dense

Cela signifie généralement traiter le canvas comme une île impérative :

import { useEffect, useRef } from "react";

export function PointCloud({ points }: { points: Float32Array }) {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const rendererRef = useRef<Renderer | null>(null);

  useEffect(() => {
    if (!canvasRef.current) return;
    rendererRef.current = new Renderer(canvasRef.current);

    return () => {
      rendererRef.current?.dispose();
      rendererRef.current = null;
    };
  }, []);

  useEffect(() => {
    rendererRef.current?.update(points);
  }, [points]);

  return <canvas ref={canvasRef} className="h-full w-full" />;
}

Ce modèle maintient React en dehors de la boucle de rendu par image, ce qui est généralement ce que vous voulez.

Les problèmes que les gens oublient

Les projets WebGL échouent souvent pour des raisons qui n'ont rien à voir avec les mathématiques des shaders :

  • La perte de contexte est réelle. Vous devez gérer webglcontextlost et webglcontextrestored.
  • Le rendu de texte est délicat. Les étiquettes nécessitent souvent une seconde couche ou une approche hybride avec le DOM.
  • Le débogage est plus lent que le travail frontend ordinaire.
  • L'accessibilité ne vient pas gratuitement.
  • Les fuites de mémoire GPU sont toujours des fuites, même si Chrome DevTools ne les rend pas aussi évidentes.

Si le produit nécessite une structure DOM sémantique, une navigation au clavier et un contenu adapté aux lecteurs d'écran pour chaque nœud visuel, une surface WebGL pure est généralement de la mauvaise forme.

Une règle pratique

Utilisez le modèle de rendu le plus simple qui peut répondre à la charge de travail.

Commencez avec le DOM ou SVG si vous le pouvez. Passer au Canvas lorsque vous avez besoin d'une boucle de dessin de bas niveau. Passez à WebGL lorsque le problème est le rendu à grande échelle et que le GPU peut réellement aider.

Cette séquence n'est pas un dogme. C'est juste une bonne façon d'éviter de résoudre un problème de document avec un pipeline graphique.

Lectures supplémentaires