Leistung von HTML5 Canvas verbessern

Boris Smus
Boris Smus

Einleitung

Das HTML5-Canvas, das als Experiment bei Apple begann, ist der am weitesten unterstützte Standard für 2D-Grafiken im unmittelbaren Modus im Web. Viele Entwickler nutzen ihn mittlerweile für eine Vielzahl von Multimedia-Projekten, Visualisierungen und Spielen. Da die von uns entwickelten Anwendungen jedoch immer komplexer werden, geraten Entwickler versehentlich an die Leistungsgrenze. Es gibt viele Weisheiten zur Optimierung der Canvas-Leistung. In diesem Artikel sollen einige davon in einer übersichtlichen Ressource für Entwickler zusammengefasst werden. Dieser Artikel enthält grundlegende Optimierungen, die für alle Computergrafikumgebungen gelten, sowie canvasspezifische Techniken, die sich im Zuge der Verbesserung von Canvas-Implementierungen ändern können. Insbesondere, wenn Browseranbieter die Canvas-GPU-Beschleunigung implementieren, werden einige der beschriebenen Leistungstechniken wahrscheinlich weniger effektiv sein. Dies wird gegebenenfalls angemerkt. Dieser Artikel bezieht sich nicht auf das HTML5-Canvas. Sehen Sie sich dazu diese Artikel zum Thema Canvas auf HTML5Rocks, dieses Kapitel auf der Website Dive into HTML5 und das Tutorial MDN Canvas an.

Leistungstests

Mit JSPerf (jsperf.com) lässt sich die sich schnell ändernde Welt des HTML5-Canvas anpassen. So lässt sich prüfen, ob alle vorgeschlagenen Optimierungen noch funktionieren. JSPerf ist eine Webanwendung, mit der Entwickler JavaScript-Leistungstests schreiben können. Jeder Test konzentriert sich auf ein Ergebnis, das Sie erreichen möchten (z. B. das Leeren des Canvas), und umfasst mehrere Ansätze, mit denen dasselbe Ergebnis erzielt wird. Janerf führt jeden Ansatz innerhalb eines kurzen Zeitraums so oft wie möglich aus und gibt eine statistisch aussagekräftige Anzahl von Iterationen pro Sekunde zurück. Höhere Werte sind immer besser. Besucher einer Seite für den Leistungstest einerJSPerf-Datei können den Test in ihrem Browser ausführen und wählen die Möglichkeit, die normalisierten Testergebnisse in Browserscope (browserscope.org) zu speichern. Da die in diesem Artikel behandelten Optimierungstechniken durch ein Ergebnis einerJSPerf gestützt werden, können Sie zurückkehren, um aktuelle Informationen darüber zu erhalten, ob das Verfahren noch angewendet wird oder nicht. Ich habe eine kleine Hilfsanwendung geschrieben, die diese Ergebnisse als Grafiken rendert und in diesen Artikel eingebettet ist.

Alle Leistungsergebnisse in diesem Artikel sind von der Browserversion abhängig. Dies stellt sich als Einschränkung heraus, da wir nicht wissen, auf welchem Betriebssystem der Browser ausgeführt wurde, und, was noch wichtiger ist, ob der HTML5-Canvas zum Zeitpunkt des Leistungstests hardwarebeschleunigt war oder nicht. Ob der HTML5-Canvas von Chrome hardwarebeschleunigt ist, kannst du durch Eingabe von about:gpu in der Adressleiste herausfinden.

Pre-Rendering auf einem Hintergrund-Canvas

Wenn Sie dem Bildschirm ähnliche Primitive über mehrere Frames hinweg neu zeichnen, wie es häufig beim Schreiben eines Spiels der Fall ist, können Sie große Leistungssteigerungen erzielen, indem Sie große Teile der Szene vorab rendern. Pre-Rendering bedeutet, dass separate nicht sichtbare Canvases zum Rendern temporärer Bilder verwendet werden. Anschließend werden die nicht sichtbaren Canvases wieder auf das sichtbare Canvas übertragen. Angenommen, Sie zeichnen Mario mit 60 Frames pro Sekunde. Sie können entweder seinen Hut, seinen Schnurrbart und das "M" für jeden Frame neu zeichnen oder Mario vorab rendern, bevor Sie die Animation ausführen. kein Pre-Rendering:

// canvas, context are defined
function render() {
  drawMario(context);
  requestAnimationFrame(render);
}

Pre-Rendering:

var m_canvas = document.createElement('canvas');
m_canvas.width = 64;
m_canvas.height = 64;
var m_context = m_canvas.getContext('2d');
drawMario(m_context);

function render() {
  context.drawImage(m_canvas, 0, 0);
  requestAnimationFrame(render);
}

Beachten Sie die Verwendung von requestAnimationFrame, die in einem späteren Abschnitt ausführlicher beschrieben wird.

Diese Methode ist besonders effektiv, wenn der Renderingvorgang (im obigen Beispiel drawMario) aufwendig ist. Ein gutes Beispiel dafür ist das Text-Rendering, das ein sehr teurer Vorgang ist.

Allerdings die schlechte Leistung des Testlaufs „Pre-Rendering Loose“. Beim Pre-Rendering muss darauf geachtet werden, dass das temporäre Canvas genau an das Bild angepasst ist, das Sie zeichnen. Andernfalls wird der Leistungsgewinn des Renderings außerhalb des Bildschirms durch den Leistungsverlust beim Kopieren eines großen Canvas auf ein anderes (der je nach Quellzielgröße variiert) gegengewichtet. Ein kleines Canvas ist im obigen Test einfach kleiner:

can2.width = 100;
can2.height = 40;

Verglichen mit dem lose gearbeiteten Modell, das eine schlechtere Leistung liefert:

can3.width = 300;
can3.height = 100;

Canvas-Aufrufe im Batch verarbeiten

Da das Zeichnen ein teurer Vorgang ist, ist es effizienter, die Zeichnungszustandsmaschine mit einem langen Satz von Befehlen zu laden und dann alle in den Videopuffer zu übertragen.

Wenn Sie beispielsweise mehrere Linien zeichnen, ist es effizienter, einen Pfad mit allen darin enthaltenen Linien zu erstellen und mit einem einzigen Aufruf zum Zeichnen zu zeichnen. Anders gesagt, anstatt separate Linien zu zeichnen:

for (var i = 0; i < points.length - 1; i++) {
  var p1 = points[i];
  var p2 = points[i+1];
  context.beginPath();
  context.moveTo(p1.x, p1.y);
  context.lineTo(p2.x, p2.y);
  context.stroke();
}

Wir erzielen eine bessere Leistung beim Zeichnen einer einzelnen Polylinie:

context.beginPath();
for (var i = 0; i < points.length - 1; i++) {
  var p1 = points[i];
  var p2 = points[i+1];
  context.moveTo(p1.x, p1.y);
  context.lineTo(p2.x, p2.y);
}
context.stroke();

Dies gilt auch für den HTML5-Canvas. Wenn Sie beispielsweise einen komplexen Pfad zeichnen, ist es besser, alle Punkte in den Pfad einzufügen, anstatt die Segmente separat zu rendern (jsperf).

Bei Canvas gibt es jedoch eine wichtige Ausnahme zu dieser Regel: Wenn die Primitive, die zum Zeichnen des gewünschten Objekts verwendet werden, kleine Begrenzungsrahmen haben (z. B. horizontale und vertikale Linien), ist es möglicherweise effizienter, sie separat zu rendern (jsperf).

Unnötige Änderungen des Canvas-Status vermeiden

Das HTML5-Canvas-Element wird auf einem Zustandsautomaten implementiert, der Füll- und Strichstile sowie frühere Punkte verfolgt, aus denen der aktuelle Pfad besteht. Bei der Optimierung der Grafikleistung ist es verlockend, sich ausschließlich auf das Grafik-Rendering zu konzentrieren. Die Manipulation des Zustandsautomaten kann jedoch auch einen Leistungsaufwand verursachen. Wenn Sie zum Rendern einer Szene beispielsweise mehrere Füllfarben verwenden, ist es kostengünstiger als nach Farbe und nicht nach Position auf dem Canvas. Um ein Nadelstreifenmuster zu rendern, können Sie z. B. einen Streifen rendern, die Farben ändern oder den nächsten Streifen rendern:

for (var i = 0; i < STRIPES; i++) {
  context.fillStyle = (i % 2 ? COLOR1 : COLOR2);
  context.fillRect(i * GAP, 0, GAP, 480);
}

Oder alle ungeraden Streifen und dann alle gerade Streifen rendern:

context.fillStyle = COLOR1;
for (var i = 0; i < STRIPES/2; i++) {
  context.fillRect((i*2) * GAP, 0, GAP, 480);
}
context.fillStyle = COLOR2;
for (var i = 0; i < STRIPES/2; i++) {
  context.fillRect((i*2+1) * GAP, 0, GAP, 480);
}

Wie erwartet ist der Zeilensprung-Ansatz langsamer, da das Ändern des Zustandsautomaten teuer ist.

Nur Bildschirmunterschiede rendern, nicht der gesamte neue Zustand

Erwartungsgemäß ist weniger Rendering auf dem Bildschirm günstiger als mehr zu rendern. Wenn es nur inkrementelle Unterschiede zwischen Neuzeichnungen gibt, können Sie die Leistung erheblich steigern, indem Sie einfach den Unterschied zeichnen. Mit anderen Worten, anstatt vor dem Zeichnen den gesamten Bildschirm zu löschen:

context.fillRect(0, 0, canvas.width, canvas.height);

Behalten Sie den gezeichneten Begrenzungsrahmen im Blick und löschen Sie nur diesen.

context.fillRect(last.x, last.y, last.width, last.height);

Wenn Sie mit Computergrafiken vertraut sind, kennen Sie diese Methode vielleicht auch als „Neu zeichnen von Bereichen“, in denen der zuvor gerenderte Begrenzungsrahmen gespeichert und bei jedem Rendering gelöscht wird. Diese Technik gilt auch für pixelbasierte Rendering-Kontexte, wie in diesem Vortrag mit dem JavaScript-Nintendo-Emulator veranschaulicht wird.

Mehrere Canvases für komplexe Szenen verwenden

Wie bereits erwähnt, ist das Zeichnen großer Bilder teuer und sollte nach Möglichkeit vermieden werden. Wie im Pre-Rendering-Abschnitt dargestellt, können wir nicht nur ein anderes Canvas für das Rendering außerhalb des Bildschirms verwenden, sondern auch Canvases, die übereinander gelegt werden. Durch die Transparenz im Vordergrund-Canvas können wir uns darauf verlassen, dass die GPU die Alphas zum Zeitpunkt des Renderings zusammensetzt. Dazu könnten Sie zwei Canvases absolut übereinander positioniert haben.

<canvas id="bg" width="640" height="480" style="position: absolute; z-index: 0">
</canvas>
<canvas id="fg" width="640" height="480" style="position: absolute; z-index: 1">
</canvas>

Der Vorteil gegenüber einem einzigen Canvas ist, dass der Hintergrund beim Zeichnen oder Löschen des Canvas im Vordergrund nicht verändert wird. Wenn sich Ihr Spiel oder Ihre Multimedia-App in einen Vorder- und einen Hintergrund unterteilen lässt, sollten Sie diese auf separaten Canvases rendern, um die Leistung erheblich zu steigern.

Häufig können Sie sich die unvollkommene menschliche Wahrnehmung ausnutzen und den Hintergrund nur einmal oder mit geringerer Geschwindigkeit als der Vordergrund rendern (was wahrscheinlich die meiste Aufmerksamkeit des Nutzers auf sich zieht). So lässt sich beispielsweise der Vordergrund jedes Mal rendern, wenn Sie etwas rendern, während der Hintergrund nur für jeden n-ten Frame gerendert wird. Beachten Sie auch, dass sich dieser Ansatz gut für eine beliebige Anzahl von zusammengesetzten Canvases verallgemeinern lässt, wenn Ihre Anwendung mit dieser Art von Struktur besser funktioniert.

shadowBlur vermeiden

Wie in vielen anderen Grafikumgebungen können Entwickler Primitiven auch beim HTML5-Canvas unkenntlich machen. Dieser Vorgang kann jedoch sehr teuer sein:

context.shadowOffsetX = 5;
context.shadowOffsetY = 5;
context.shadowBlur = 4;
context.shadowColor = 'rgba(255, 0, 0, 0.5)';
context.fillRect(20, 20, 150, 100);

Verschiedene Möglichkeiten zum Löschen des Canvas

Da es sich beim HTML5-Canvas um ein Zeichenmodell im Sofortmodus handelt, muss die Szene für jeden Frame neu gezeichnet werden. Aus diesem Grund ist das Löschen des Canvas ein von grundlegender Bedeutung für HTML5-Canvas-Apps und -Spiele. Wie im Abschnitt Änderungen des Canvas-Status vermeiden erwähnt, ist es häufig nicht wünschenswert, den gesamten Canvas zu löschen. Wenn Sie dies jedoch müssen, gibt es zwei Möglichkeiten: Sie können context.clearRect(0, 0, width, height) aufrufen oder einen Canvas-spezifischen Hack verwenden: canvas.width = canvas.width. Beim Schreiben übertrifft clearRect in der Regel die Version mit dem Zurücksetzen auf die Breite. In einigen Fällen ist das Zurücksetzen des Hacks canvas.width in Chrome 14 jedoch deutlich schneller.

Seien Sie bei diesem Tipp vorsichtig, da er stark von der zugrunde liegenden Canvas-Implementierung abhängt und häufig geändert wird. Weitere Informationen finden Sie im Artikel von Simon Sarris zum Löschen des Canvas.

Gleitkommakoordinaten vermeiden

Das HTML5-Canvas unterstützt Subpixel-Rendering und kann nicht deaktiviert werden. Wenn Sie mit Koordinaten zeichnen, die keine Ganzzahlen sind, wird automatisch Anti-Aliasing verwendet, um die Linien zu glätten. Hier ist der visuelle Effekt, der aus diesem Artikel zur Subpixel-Canvas-Leistung von Seb Lee-Delisle entnommen wurde:

Subpixel

Wenn das geglättete Sprite nicht der gewünschte Effekt ist, lassen sich die Koordinaten mit Math.floor oder Math.round (jsperf) viel schneller in Ganzzahlen umwandeln:

Zum Konvertieren Ihrer Gleitkommakoordinaten in Ganzzahlen können Sie mehrere clevere Techniken anwenden. Die leistungsstärkste ist das Addieren der Hälfte zur Zielzahl und das anschließende Ausführen bitweiser Operationen am Ergebnis, um den Bruchteil zu entfernen.

// With a bitwise or.
rounded = (0.5 + somenum) | 0;
// A double bitwise not.
rounded = ~~ (0.5 + somenum);
// Finally, a left bitwise shift.
rounded = (0.5 + somenum) << 0;

Eine vollständige Leistungsaufschlüsselung finden Sie hier (jsperf).

Beachten Sie, dass diese Art der Optimierung keine Rolle mehr spielt, sobald Canvas-Implementierungen GPU-beschleunigt sind, sodass nicht ganzzahlige Koordinaten schnell gerendert werden können.

Animationen mit requestAnimationFrame optimieren

Die relativ neue requestAnimationFrame API ist die empfohlene Methode zum Implementieren interaktiver Anwendungen im Browser. Anstatt den Browser so einzustellen, dass er mit einer bestimmten festen Tickrate gerendert wird, bitten Sie den Browser höflich, Ihre Rendering-Routine aufzurufen. Sie werden dann angerufen, sobald der Browser verfügbar ist. Ein weiterer Vorteil: Wenn sich die Seite nicht im Vordergrund befindet, rendert der Browser nicht. Der Callback requestAnimationFrame zielt auf eine Callback-Rate von 60 fps ab, kann diese aber nicht garantieren. Daher müssen Sie im Auge behalten, wie viel Zeit seit dem letzten Rendering vergangen ist. Das kann etwa so aussehen:

var x = 100;
var y = 100;
var lastRender = Date.now();
function render() {
  var delta = Date.now() - lastRender;
  x += delta;
  y += delta;
  context.fillRect(x, y, W, H);
  requestAnimationFrame(render);
}
render();

Beachten Sie, dass diese Verwendung von requestAnimationFrame sowohl für Canvas als auch für andere Rendering-Technologien wie WebGL gilt. Zum Zeitpunkt der Erstellung dieses Dokuments steht diese API nur in Chrome, Safari und Firefox zur Verfügung, sodass Sie diesen Shim verwenden sollten.

Die meisten mobilen Canvas-Implementierungen sind langsam

Sprechen wir über Mobilgeräte. Zum Zeitpunkt der Entstehung dieses Artikels war nur die iOS 5.0-Beta mit Safari 5.1 eine GPU-beschleunigte mobile Canvas-Implementierung. Ohne die GPU-Beschleunigung haben mobile Browser im Allgemeinen nicht genug leistungsfähige CPUs für moderne Canvas-basierte Anwendungen. Eine Reihe der oben beschriebenen JSP-Tests schneidet auf Mobilgeräten im Vergleich zu Desktop-Computern um eine Größenordnung schlechter ab, was die Arten von geräteübergreifenden Apps, die erfolgreich ausgeführt werden können, stark einschränkt.

Fazit

In diesem Artikel wurde eine umfassende Reihe nützlicher Optimierungstechniken behandelt, mit denen Sie leistungsstarke HTML5-Canvas-basierte Projekte entwickeln können. Jetzt, da Sie etwas Neues gelernt haben, machen Sie Ihre tollen Kreationen weiter. Wenn Sie derzeit kein Spiel oder keine Anwendung haben, die optimiert werden kann, können Sie sich von Chrome-Tests und Creative JS inspirieren lassen.

Verweise