almessadi.
Zur Übersicht

Wann WebGL den DOM beim Rendering komplexer UIs übertrifft_

Wie man erkennt, wann der DOM, SVG oder Canvas nicht mehr skalieren und wann WebGL der bessere Rendering-Pfad für komplexe, datenintensive Schnittstellen wird.

Veröffentlicht4. März 2024
Lesezeit21 min read

Die meisten Frontend-Performanceprobleme erfordern kein WebGL.

Wenn Sie einen Kartenstapel, ein Tooltip oder sogar ein mäßig komplexes Diagramm animieren, ist der DOM normalerweise die richtige Abstraktion. Er ist einfacher zu debuggen, einfacher zugänglich zu machen und einfacher mit dem Rest Ihres Produkts zu integrieren.

WebGL wird interessant, wenn der Browser mehr Zeit damit verbringt, Tausende visueller Primitiven zu verwalten, als Ihre Anwendung mit der eigentlichen Produktlogik. Das zeigt sich normalerweise als eine Kombination dieser Symptome:

  • Frame-Zeiten-Spitzen während der Stilneuberechnung oder des Malens
  • SVG-Knoten oder Canvas-Zeichnungsaufrufe wachsen in die Zehntausende
  • Interaktionen werden merklich schlechter auf gewöhnlichen Laptops, nicht nur auf Ihrer Entwicklermaschine
  • Die UI ist visuell einfach, aber die Renderinglast ist groß

Das ist der Punkt, an dem "den React-Komponenten optimieren" nicht mehr als ernsthafter Plan gilt.

Was sich ändert, wenn Sie zu WebGL wechseln

Bei der DOM- oder SVG-Darstellung erledigt der Browser eine Menge Arbeit für Sie:

  • Layout
  • Stilauflösung
  • Hit-Test
  • Malen
  • Komposition

Dieser Komfort hat seinen Preis. Jedes Element beteiligt sich an den Browser-Pipelines, die zuerst für Dokumente und zweitens für Grafiken entworfen wurden.

WebGL geht den entgegengesetzten Kompromiss ein. Sie laden Geometrie und per-Instanz-Daten in GPU-Puffer hoch, schreiben Shaders, um der GPU mitzuteilen, wie sie zeichnen soll, und halten den Browser aus dem kritischen Pfad heraus. Sie verlieren an Komfort, gewinnen aber Kontrolle über den Durchsatz.

Für dichte, animierte Szenen kann dieser Tausch lohnenswert sein.

Das Leistungsproblem, das Sie tatsächlich lösen

Der häufigste Fehler ist zu denken, dass "WebGL schneller" eine allgemeine Regel ist. Das ist es nicht.

WebGL ist schneller, wenn:

  • die Szene viele ähnliche visuelle Objekte enthält
  • diese Objekte als strukturierte numerische Daten dargestellt werden können
  • die Arbeit hauptsächlich aus Zeichnen, Interpolation, Transformationen oder Farbberechnungen besteht

WebGL ist oft das falsche Werkzeug, wenn:

  • die Szene hauptsächlich aus Text besteht
  • Zugänglichkeitssemantiken auf Elementebene wichtig sind
  • Sie das native Layoutverhalten des Browsers benötigen
  • die Komplexität im Datenabrufen oder Zustandwechsel liegt, nicht im Rendering

Ein gutes Beispiel ist eine Telemetrieansicht mit Tausenden beweglicher Punkte. Ein schlechtes Beispiel ist eine Marketingseite mit sechs dekorativen Blasen.

Ein minimales WebGL-Setup

Der erste Schritt besteht darin, eine vorhersehbare Render-Oberfläche zu erhalten:

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

if (!gl) {
  throw new Error("WebGL2 ist in diesem Browser nicht verfügbar");
}

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);

Hier sind zwei Details wichtig:

  • Sie dimensionieren den Backing-Puffer mit devicePixelRatio, nicht nur mit CSS-Pixeln.
  • Sie rufen gl.viewport(...) nach der Größenänderung auf, oder Ihre Szene wird mit veralteten Dimensionen gerendert.

Der eigentliche Gewinn: Daten auf der GPU halten

Die größte Verbesserung der WebGL-Leistung stammt normalerweise nicht von einem cleveren Shader. Sie kommt davon, unnötigen CPU-GPU-Verkehr zu vermeiden.

Dies ist die Art von Aktualisierungspfad, die still und heimlich die Leistung killt:

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

Es allokiert Ressourcen im kritischen Pfad neu. Das ist das GPU-Pendant zum Neuanlegen der Welt in jedem Frame.

Ein gesünderes Muster ist es, einmal zu allokieren und vor Ort zu aktualisieren:

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

Wenn die Anzahl der Punkte begrenzt ist, ist die Voraballokation normalerweise das richtige Standardverfahren.

Shader sind nur Programme

WebGL erscheint einschüchternd, bis Sie aufhören, Shader als "Grafikmagie" zu betrachten und anfangen, sie als kleine Programme mit strengen Eingaben und Ausgaben zu betrachten.

Ein Vertex-Shader entscheidet, wo etwas auf dem Bildschirm landet:

#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;
}

Ein Fragment-Shader entscheidet, wie die resultierenden Pixel aussehen:

#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);
}

Das reicht aus, um weiche, kreisförmige Punkte zu rendern, ohne den DOM zu bitten, Tausende von Elementen zu verwalten.

Wo React immer noch passt

Das Rendern zu WebGL zu verschieben, bedeutet nicht, React aus der Anwendung zu entfernen.

Die saubere Trennung lautet:

  • React verwaltet Steuerungen, Filter, Layout und Datenladungen
  • WebGL verwaltet die dichte Zeichenoberfläche

Das bedeutet normalerweise, die Leinwand als imperativen Raum zu behandeln:

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" />;
}

Dieses Muster hält React aus der Rendering-Schleife pro Frame heraus, was normalerweise gewünscht ist.

Die Probleme, die die Leute vergessen

WebGL-Projekte scheitern oft aus Gründen, die nichts mit Shader-Mathe zu tun haben:

  • Kontextverlust ist real. Sie müssen webglcontextlost und webglcontextrestored behandeln.
  • Textdarstellung ist ungeschickt. Labels benötigen häufig eine zweite Ebene oder einen hybriden DOM-Ansatz.
  • Debugging ist langsamer als reguläre Frontend-Arbeit.
  • Zugänglichkeit kommt nicht kostenlos.
  • GPU-Speicherlecks sind immer noch Lecks, auch wenn die Chrome DevTools sie nicht so offensichtlich machen.

Wenn das Produkt eine semantische DOM-Struktur, Tastaturnavigation und screenreaderfreundliche Inhalte für jedes visuelle Element benötigt, ist eine reine WebGL-Oberfläche normalerweise die falsche Form.

Eine praktische Regel

Verwenden Sie das einfachste Render-Modell, das die Arbeitslast bewältigen kann.

Beginnen Sie mit dem DOM oder SVG, wenn Sie können. Wechseln Sie zu Canvas, wenn Sie eine niedrigere Zeichenschleife benötigen. Wechseln Sie zu WebGL, wenn das Problem die großflächige Rendering ist und die GPU tatsächlich helfen kann.

Diese Reihenfolge ist keine Dogma. Es ist nur eine gute Möglichkeit, ein Dokumentproblem nicht mit einer Grafikpipeline zu lösen.

Weitere Lektüre