Leistung von HTML5 Canvas verbessern

Boris Smus
Boris Smus

Einführung

HTML5 Canvas, das ursprünglich als Experiment von Apple entwickelt wurde, ist der am weitesten unterstützte Standard für 2D-Grafik im Immediate-Mode im Web. Viele Entwickler nutzen es jetzt für eine Vielzahl von Multimediaprojekten, Visualisierungen und Spielen. Da die von uns entwickelten Anwendungen jedoch immer komplexer werden, stoßen Entwickler unweigerlich auf Leistungsprobleme. Es gibt viele verschiedene Empfehlungen zur Optimierung der Canvas-Leistung. In diesem Artikel soll ein Teil dieses Wissens in einer für Entwickler leichter verdaulichen Ressource zusammengefasst werden. Dieser Artikel enthält grundlegende Optimierungen, die für alle Computergrafikumgebungen gelten, sowie Canvas-spezifische Techniken, die sich mit der Verbesserung von Canvas-Implementierungen ändern können. Insbesondere wenn Browseranbieter die Canvas-GPU-Beschleunigung implementieren, werden einige der beschriebenen Leistungstechniken wahrscheinlich weniger effektiv. Dies wird gegebenenfalls vermerkt. In diesem Artikel geht es nicht um die Verwendung von HTML5-Canvas. Sehen Sie sich dazu die Canvas-Artikel auf HTML5Rocks, dieses Kapitel auf der Website „Dive into HTML5“ oder die MDN Canvas-Anleitung an.

Leistungstests

Aufgrund der sich schnell ändernden Welt des HTML5-Canvas wird mit JSPerf (jsperf.com) überprüft, ob jede vorgeschlagene Optimierung weiterhin funktioniert. 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 Löschen des Canvas), und umfasst mehrere Ansätze, mit denen dasselbe Ergebnis erzielt wird. JSPerf führt jeden Ansatz innerhalb eines kurzen Zeitraums so oft wie möglich aus und gibt eine statistisch signifikante Anzahl von Iterationen pro Sekunde an. Je höher die Punktzahl, desto besser. Besucher einer JSPerf-Leistungstestseite können den Test in ihrem Browser ausführen und JSPerf die normalisierten Testergebnisse bei Browserscope (browserscope.org) speichern lassen. Da die Optimierungstechniken in diesem Artikel durch ein JSPerf-Ergebnis gestützt werden, können Sie jederzeit aktuelle Informationen dazu abrufen, ob die jeweilige Technik noch relevant ist. Ich habe eine kleine Hilfsanwendung geschrieben, die diese Ergebnisse als Grafiken darstellt, die in diesen Artikel eingebettet sind.

Alle Leistungsergebnisse in diesem Artikel beziehen sich auf die Browserversion. Das ist eine Einschränkung, da wir nicht wissen, unter welchem Betriebssystem der Browser ausgeführt wurde, und vor allem nicht, ob HTML5 Canvas beim Leistungstest hardwarebeschleunigt wurde. Ob der HTML5-Canvas von Chrome hardwarebeschleunigt ist, können Sie herausfinden, indem Sie about:gpu in die Adressleiste eingeben.

Vorab-Rendering auf eine Off-Screen-Canvas

Wenn Sie ähnliche Primitive in mehreren Frames auf dem Bildschirm neu zeichnen, wie es beim Erstellen eines Spiels oft der Fall ist, können Sie durch das Vorab-Rendering großer Teile der Szene große Leistungssteigerungen erzielen. Beim Pre-Rendering wird ein separates Off-Screen-Canvas (oder mehrere Off-Screen-Canvasse) verwendet, auf dem temporäre Bilder gerendert werden. Anschließend werden die Off-Screen-Canvasse auf das sichtbare Canvas gerendert. Angenommen, Sie zeichnen Mario, der mit 60 Frames pro Sekunde läuft, neu. Sie können entweder seinen Hut, seinen Schnurrbart und das „M“ in jedem Frame neu zeichnen oder Mario vor dem Ausführen der Animation vorrendern. ohne 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 genauer erläutert wird.

Diese Technik ist besonders effektiv, wenn der Renderingvorgang (drawMario im obigen Beispiel) teuer ist. Ein gutes Beispiel hierfür ist das Text-Rendering, ein sehr ressourcenintensiver Vorgang.

Die schlechte Leistung des Testfalls „Vorab gerendert, locker“ Achten Sie beim Vorab-Rendering darauf, dass der temporäre Canvas genau um das Bild passt, das Sie zeichnen. Andernfalls wird der Leistungsgewinn durch das Offscreen-Rendering durch den Leistungsverlust beim Kopieren eines großen Canvas in einen anderen kompensiert (dies variiert je nach Größe des Quell- und Zielobjekts). Ein enges Canvas im obigen Test ist einfach kleiner:

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

Im Vergleich zur lockeren Verpackung, die eine schlechtere Leistung erzielt:

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

Canvas-Aufrufe im Batch zusammenfassen

Da das Zeichnen ein teurer Vorgang ist, ist es effizienter, den Zustandsautomaten für das Zeichnen mit einer langen Reihe von Befehlen zu laden und diese dann alle in den Videopuffer zu übertragen.

Wenn Sie beispielsweise mehrere Linien zeichnen, ist es effizienter, einen Pfad mit allen Linien zu erstellen und ihn mit einem einzigen Draw-Aufruf zu zeichnen. Mit anderen Worten: Statt 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();
}

Die Leistung ist beim Zeichnen einer einzelnen Polylinie besser:

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

Das gilt auch für HTML5-Canvas. Beim Zeichnen eines komplexen Pfades ist es beispielsweise besser, alle Punkte in den Pfad aufzunehmen, anstatt die Segmente separat zu rendern (jsperf).

Bei Canvas gibt es jedoch eine wichtige Ausnahme von dieser Regel: Wenn die Primitiven, die zum Zeichnen des gewünschten Objekts verwendet werden, kleine Begrenzungsboxen 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 über einem Zustandsautomaten implementiert, der Dinge wie Füll- und Strichstile sowie vorherige Punkte erfasst, die den aktuellen Pfad bilden. Wenn Sie die Grafikleistung optimieren möchten, ist es verlockend, sich nur auf das Grafik-Rendering zu konzentrieren. Die Manipulation des Zustandsautomaten kann jedoch auch einen Leistungsoverhead verursachen. Wenn Sie beispielsweise mehrere Füllungsfarben zum Rendern einer Szene verwenden, ist es günstiger, nach Farbe statt nach Platzierung auf dem Canvas zu rendern. Um ein Nadelstreifenmuster zu rendern, können Sie einen Streifen rendern, die Farben ändern, den nächsten Streifen rendern usw.:

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

Oder Sie können alle ungeraden und dann alle geraden 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 Interlaced-Ansatz langsamer, da das Ändern des Zustandsautomaten aufwendig ist.

Nur Bildschirmunterschiede rendern, nicht den gesamten neuen Status

Wie zu erwarten, ist es günstiger, weniger auf dem Bildschirm zu rendern als mehr. Wenn die Unterschiede zwischen den einzelnen Bildern nur geringfügig sind, können Sie die Leistung erheblich steigern, indem Sie nur die Differenz zeichnen. Anstatt vor dem Zeichnen den gesamten Bildschirm zu löschen, können Sie Folgendes tun:

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

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

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

Wenn Sie mit Computergrafiken vertraut sind, kennen Sie dieses Verfahren möglicherweise auch als „Neuzeichnen von Bereichen“. Dabei wird der zuvor gerenderte Begrenzungsrahmen gespeichert und bei jedem Rendern gelöscht. Diese Technik gilt auch für pixelbasierte Rendering-Kontexte, wie in diesem JavaScript-Vortrag zum Nintendo-Emulator veranschaulicht.

Für komplexe Szenen mehrere Ebenen verwenden

Wie bereits erwähnt, ist das Zeichnen großer Bilder teuer und sollte nach Möglichkeit vermieden werden. Neben der Verwendung eines anderen Canvas für das Rendering außerhalb des Bildschirms, wie im Abschnitt zum Pre-Rendering dargestellt, können wir auch Canvasse übereinander legen. Durch die Verwendung von Transparenz im Vordergrund-Canvas können wir die Alphas bei der Renderung mit der GPU zusammensetzen. Sie könnten das so einrichten: Zwei Canvasse, die absolut positioniert sind, übereinander.

<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 nur einem Canvas besteht darin, dass wir den Hintergrund nicht ändern, wenn wir den Vordergrund-Canvas zeichnen oder löschen. Wenn Ihr Spiel oder Ihre Multimedia-App in Vordergrund und Hintergrund unterteilt werden kann, sollten Sie diese auf separaten Canvases rendern, um eine erhebliche Leistungssteigerung zu erzielen.

Sie können oft von der unperfekten menschlichen Wahrnehmung profitieren und den Hintergrund nur einmal oder mit einer langsameren Geschwindigkeit als den Vordergrund rendern, der wahrscheinlich die meiste Aufmerksamkeit der Nutzer auf sich zieht. So können Sie beispielsweise den Vordergrund bei jedem Rendern, den Hintergrund aber nur bei jedem n-ten Frame rendern. Beachten Sie auch, dass dieser Ansatz für beliebig viele zusammengesetzte Canvasse geeignet ist, wenn Ihre Anwendung mit dieser Art von Struktur besser funktioniert.

shadowBlur vermeiden

Wie in vielen anderen Grafikumgebungen können Entwickler mit HTML5-Canvas Primitiven unscharfstellen. Dieser Vorgang kann jedoch sehr aufwendig 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 Leeren des Canvas

Da HTML5-Canvas ein Immediate-Mode-Zeichenparadigma ist, muss die Szene in jedem Frame explizit neu gezeichnet werden. Daher ist das Löschen des Canvas für HTML5-Canvas-Apps und -Spiele von grundlegender Bedeutung. Wie im Abschnitt Canvas-Statusänderungen vermeiden erwähnt, ist es oft nicht wünschenswert, den gesamten Canvas zu löschen. Wenn Sie es aber müssen, haben Sie zwei Möglichkeiten: Sie können context.clearRect(0, 0, width, height) aufrufen oder einen Canvas-spezifischen Hack verwenden: canvas.width = canvas.width. Zum Zeitpunkt der Erstellung dieses Artikels ist clearRect im Allgemeinen schneller als die Version mit zurückgesetzter Breite. In einigen Fällen ist die Verwendung des canvas.width-Reset-Hacks in Chrome 14 jedoch deutlich schneller.

Seien Sie mit diesem Tipp vorsichtig, da er stark von der zugrunde liegenden Canvas-Implementierung abhängt und sich leicht ändern kann. Weitere Informationen finden Sie im Artikel von Simon Sarris zum Leeren des Canvas.

Gleitkommakoordinaten vermeiden

HTML5-Canvas unterstützt das Rendering unter Pixeln 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 aus diesem Artikel von Seb Lee-Delisle zur Leistung von Canvas-Pixeln:

Subpixel

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

Es gibt mehrere clevere Methoden, um Ihre Gleitkommakoordinaten in Ganzzahlen umzuwandeln. Die leistungsstärkste Methode besteht darin, der Zielzahl eine Hälfte hinzuzufügen und dann Bitweise-Operationen auf das Ergebnis anzuwenden, 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;

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

Diese Art der Optimierung sollte nicht mehr erforderlich sein, sobald Canvas-Implementierungen GPU-beschleunigt sind und nicht ganzzahlige Koordinaten schnell gerendert werden können.

Animationen mit requestAnimationFrame optimieren

Die relativ neue requestAnimationFrame API ist die empfohlene Methode zur Implementierung interaktiver Anwendungen im Browser. Anstatt den Browser zum Rendern mit einer bestimmten festen Tickrate aufzufordern, bitten Sie den Browser, Ihre Rendering-Routine aufzurufen und aufgerufen zu werden, wenn der Browser verfügbar ist. Als angenehmer Nebeneffekt wird die Seite nicht gerendert, wenn sie sich nicht im Vordergrund befindet. Der requestAnimationFrame-Callback zielt auf eine Callback-Rate von 60 fps ab, kann dies aber nicht garantieren. Du musst also im Blick behalten, wie viel Zeit seit dem letzten Rendern vergangen ist. Das könnte 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();

Diese Verwendung von requestAnimationFrame gilt sowohl für Canvas als auch für andere Rendering-Technologien wie WebGL. Zum Zeitpunkt der Erstellung dieses Artikels ist diese API nur in Chrome, Safari und Firefox verfügbar. Daher solltest du diesen Shim verwenden.

Die meisten Canvas-Implementierungen für Mobilgeräte sind langsam

Sprechen wir über Mobilgeräte. Zum Zeitpunkt der Erstellung dieses Artikels ist die GPU-beschleunigte Implementierung von Canvas für Mobilgeräte nur in der iOS 5.0-Betaversion mit Safari 5.1 verfügbar. Ohne GPU-Beschleunigung haben mobile Browser in der Regel nicht leistungsstark genug CPUs für moderne Canvas-basierte Anwendungen. Bei einigen der oben beschriebenen JSPerf-Tests ist die Leistung auf Mobilgeräten um ein Vielfaches schlechter als auf Computern. Das schränkt die Art der geräteübergreifenden Apps ein, die Sie erfolgreich ausführen können.

Fazit

Zusammenfassend wurden in diesem Artikel eine Reihe nützlicher Optimierungstechniken vorgestellt, mit denen Sie leistungsstarke HTML5-Canvas-basierte Projekte entwickeln können. Jetzt, da du etwas Neues gelernt hast, kannst du deine tollen Kreationen optimieren. Wenn Sie derzeit kein Spiel oder keine Anwendung optimieren möchten, können Sie sich Chrome-Experimente und Creative JS ansehen.

Verweise