Leistungsverbesserung des HTML5-Canvas

HTML5 Rocks

Einführung

Das HTML5-Canvas begann als Experiment von Apple und ist der am meisten unterstützte Webstandard für 2D-Grafiken im unmittelbaren Modus. Viele Entwickler vertrauen heute darauf und nutzen ihn für verschiedenste Multimedia-Projekte, Visualisierungen und Spiele. Da die von uns entwickelten Anwendungen jedoch immer komplexer werden, stoßen die Entwickler unvermeidlich an die Leistungsgrenze.

Es gibt viele Weisheiten zur Optimierung der Canvas-Leistung. In diesem Artikel sollen einige davon in einer kompakten Ressource für Entwickler zusammengefasst werden. Dieser Artikel beschreibt grundlegende Optimierungsmöglichkeiten, die sich auf alle Computergrafikumgebungen anwenden lassen, sowie canvasspezifische Techniken, die im Zuge von verbesserten Canvas-Implementierungen Änderungen unterliegen. Besonders durch die Implementierung der Canvas-GPU-Beschleunigung vonseiten der Browser-Anbieter verlieren einige der beschriebenen Leistungstechniken möglicherweise einen Teil ihrer Wirkung. Darauf wird, falls zutreffend, an entsprechender Stelle hingewiesen.

Beachten Sie, dass dieser Artikel nicht auf die Verwendung des HTML5-Canvas eingeht. Informationen dazu finden Sie in diesen Artikeln zum Thema Canvas auf HTML5Rocks, in diesem Kapitel zu den Details von HTML5 sowie in dieser MDN-Anleitung.

Leistungstest

Die Welt des HTML5-Canvas dreht sich schnell. Mit JSPerf (jsperf.com) lässt sich testen, ob die vorgeschlagenen Optimierungen noch funktionieren. JSPerf ist eine Webanwendung, mit der Entwickler JavaScript-Leistungstests schreiben können. Jeder Test zielt auf ein Ergebnis ab, das Sie erreichen möchten, zum Beispiel das Löschen des Canvas, und verfolgt mehrere Ansätze, die zum gleichen Ergebnis führen. JSPerf führt jeden Ansatz innerhalb eines kurzen Zeitraums so oft wie möglich aus und gibt eine strategisch aussagekräftige Zahl von Wiederholungen pro Sekunde zurück. Je höher die Zahl, desto besser!

Nutzer, die eine Seite mit einem JSPerf-Leistungstest besuchen, können den Test in ihrem Browser ausführen und festlegen, dass JSPerf die normalisierten Testergebnisse in Browserscope (browserscope.org) speichert. Da die in diesem Artikel beschriebenen Optimierungstechniken durch ein JSPerf-Ergebnis gestützt werden, können Sie jederzeit anhand der aktuellen Informationen herausfinden, ob die Technik noch infrage kommt. Ich habe eine kleine Hilfeanwendung geschrieben, die diese Ergebnisse in Grafiken darstellt. Diese habe ich in diesen Artikel eingebettet.

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 das HTML5-Canvas zum Zeitpunkt des Leistungstests hardwarebeschleunigt war. Durch Eingabe von about:gpu in die Adressleiste können Sie herausfinden, ob das HTML5-Canvas von Chrome hardwarebeschleunigt ist.

Pre-Rendering in ein Hintergrund-Canvas

Wenn Sie über mehrere Frames hinweg immer wieder ähnliche Primitive zeichnen, zum Beispiel beim Schreiben eines Spiels, können Sie die Leistung merklich steigern, indem Sie einen Großteil der Szene vorab rendern. Pre-Rendering bedeutet, dass separate Hintergrund-Canvases zum Rendern temporärer Bilder verwendet werden. Anschließend werden die Hintergrund-Canvases wieder auf das sichtbare Canvas übertragen. Denjenigen, die sich mit Computergrafiken auskennen, ist diese Technik auch als Displayliste bekannt.

Nehmen wir zum Beispiel an, 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.

Ohne Pre-Rendering:

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

Mit 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, auf die ich im weiteren Verlauf detaillierter eingehe. Die folgende Grafik zeigt die Leistungsvorteile im Falle von Pre-Rendering (aus diesem jsperf-Test):

Diese Technik ist besonders effektiv, wenn der Rendering-Vorgang, im oben stehenden Beispiel drawMario, aufwendig ist. Ein gutes Beispiel in diesem Zusammenhang ist das Text-Rendering, das ein sehr aufwendiger Prozess ist. Beim Pre-Rendering ist folgende drastische Leistungssteigerung zu erwarten (aus diesem jsperf-Test):

Beachten Sie jedoch die schlechte Leistung des Testfalls mit dem großzügigen Canvas (Pre-Rendered Loose) im oben stehenden Beispiel. Beim Pre-Rendering müssen Sie darauf achten, dass die Größe Ihres temporären Canvas genau dem Bild angepasst ist, das Sie zeichnen. Andernfalls wird der Leistungsschub des Hintergrund-Rendering durch den Leistungsverlust beim Kopieren eines großen Canvas auf ein anderes wieder wettgemacht (variiert in Abhängigkeit von Quell-/Zielgröße). Ein passendes Canvas wie im obigen Test ist einfach kleiner:

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

Verglichen mit dem großzügigen Canvas, das eine schlechtere Leistung erzielt:

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

Canvas-Aufrufe im Batch verarbeiten

Da das Zeichnen ein aufwendiger Vorgang ist, ist es effizienter, den Zustandsautomaten mit einer langen Reihe von Befehlen zu füttern, die anschließend im Video-Zwischenspeicher abgelegt werden.

Wenn Sie zum Beispiel mehrere Linien zeichnen, sollten Sie einen Pfad mit sämtlichen Linien erstellen und im Rahmen eines einzigen Aufrufs zeichnen. Also anstelle separater Linien:

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

Erzielen Sie durch Zeichnen einer einzelnen Polylinie eine bessere Leistung:

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 gleiche Prinzip gilt auch für das HTML5-Canvas. Wenn Sie beispielsweise einen komplexen Pfad zeichnen, sollten Sie dem Pfad sämtliche Punkte zuweisen, anstatt alle Segmente separat zu rendern (jsperf).

Beachten Sie jedoch, dass es bei Canvas eine entscheidende Ausnahme zu dieser Regel gibt: Wenn die zum Zeichnen des gewünschten Objekts verwendeten Primitiven kleine Begrenzungsrahmen aufweisen, zum Beispiel horizontale und vertikale Linien, ist es möglicherweise effizienter, diese einzeln zu rendern (jsperf):

Unnötige Änderungen des Zustandsautomaten vermeiden

Das HTML5-Canvas-Element wird auf einem Zustandsautomaten implementiert, der Elemente wie Füll- und Strichstile sowie vorherige Punkte verfolgt, aus denen sich der aktuelle Pfad zusammensetzt. Bei einer Optimierung der Grafikleistung neigt dieser dazu, sich ausschließlich auf das Rendern der Grafik zu konzentrieren. Durch eine Manipulation des Zustandsautomaten kann aber auch ein Leistungsüberschuss entstehen.

Wenn Sie zum Rendern einer Szene zum Beispiel mehrere Füllfarben verwenden, ist es günstiger, nach Farben und nicht nach Position auf dem Canvas zu rendern. Zum Rendern eines Nadelstreifenmusters könnten 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 zuerst alle ungeraden und anschließend 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);
}

Beim folgenden Leistungstest wird ein verschachteltes Nadelstreifenmuster mithilfe dieser beiden Ansätze gezeichnet (jsperf):

Wie erwartet ist das Zeilensprungverfahren langsamer, da das Ändern des Zustandsautomaten zeitaufwendig ist.

Rendern von Bildschirmunterschieden anstelle des gesamten neuen Zustands

Erwartungsgemäß ist weniger rendern auf dem Bildschirm besser als mehr. Wenn zwischen den einzelnen Zeichnungen nur marginale Unterschiede bestehen, können Sie die Leistung beträchtlich steigern, indem Sie einfach nur den Unterschied zeichnen. Mit anderen Worten: Anstatt den gesamten Bildschirm vor dem Zeichnen zu löschen:

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

Nehmen Sie den gezeichneten Begrenzungsrahmen als Grundlage und löschen Sie nur diesen.

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

Dies wird im nachfolgenden Leistungstest dargestellt, bei dem sich ein weißer Punkt über den Bildschirm bewegt (jsperf):

Wenn Sie sich mit Computergrafiken auskennen, ist Ihnen diese Technik möglicherweise unter dem Schlagwort "Neuzeichnen von Regionen" bekannt. Dabei wird der zuvor gerenderte Begrenzungsrahmen gespeichert und dann bei jedem Rendern gelöscht.

Diese Technik findet auch in pixelbasierten Rendering-Umgebungen Anwendung, wie dieser Vortrag zu einem JavaScript Nintendo-Emulator zeigt.

Mehrere Canvas-Schichten für komplexe Szenen verwenden

Wie bereits erwähnt, ist das Zeichnen großer Bilder zeitaufwendig und sollte möglichst vermieden werden. Neben der Möglichkeit, ein anderes Canvas für das Hintergrund-Rendering zu verwenden, wie im Abschnitt zum Pre-Rendering erläutert, können auch mehrere Canvases übereinandergelegt werden. Indem wir das vordergründige Canvas transparent darstellen, kann der Grafikprozessor die Alphas beim Rendering zusammensetzen. Sie können dabei wie folgt vorgehen, indem Sie zwei Canvases exakt übereinanderlegen.

<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 im Vergleich zu einem einzigen Canvas ist der, dass beim Zeichnen oder Löschen des vordergründigen Canvas der Hintergrund unverändert bleibt. Wenn sich Ihr Spiel oder Ihre Multimedia-App in einen Vorder- und einen Hintergrund unterteilen lässt, sollten Sie in Betracht ziehen, diese auf separaten Canvases zu rendern. So können Sie eine beträchtliche Leistungssteigerung erzielen. In der folgenden Grafik wird der Ansatz mit nur einem Canvas mit einem Ansatz verglichen, bei dem Sie lediglich den Vordergrund neu zeichnen und löschen (jsperf):

Häufig können Sie sich die unvollkommene menschliche Wahrnehmung zunutze machen und den Hintergrund nur einmal oder im Vergleich zum Vordergrund mit geringerer Geschwindigkeit rendern, da Letzterer wahrscheinlich den größten Teil der Aufmerksamkeit des Nutzers auf sich zieht. Zum Beispiel können Sie den Vordergrund jedes Mal rendern, den Hintergrund jedoch nur jeden n-ten Frame.

Beachten Sie zudem, dass sich dieser Ansatz für eine beliebige Anzahl an zusammengesetzten Canvases verallgemeinern lässt, sofern Ihre Anwendung mit dieser Struktur besser funktioniert.

shadowBlur vermeiden

Wie in vielen anderen Grafikumgebungen haben Entwickler beim HTML5-Canvas die Möglichkeit, Primitiven weichzuzeichnen. 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);

Der folgende Leistungstest zeigt die gleiche Szene. Diese wurde einmal mit und einmal ohne Schatten gerendert. Dabei ergeben sich beträchtliche Leistungsunterschiede (jsperf):

Mehrere Möglichkeiten zum Löschen des Canvas

Da es sich beim HTML5-Canvas um ein Zeichenmodell im unmittelbaren Modus handelt, muss die Szene für jeden Frame neu gezeichnet werden. Aus diesem Grund ist das Löschen des Canvas ein sehr wichtiger Vorgang für HTML5-Canvas-Apps und -Spiele.

Wie bereits im Abschnitt Unnötige Änderungen des Zustandsautomaten vermeiden erwähnt, wird das Löschen des gesamten Canvas meist nicht empfohlen. Sollte jedoch die Notwendigkeit bestehen, haben Sie zwei Möglichkeiten: Aufrufen von context.clearRect(0, 0, width, height) oder Verwenden eines canvasspezifischen Hacks: canvas.width = canvas.width;.

Beim Schreiben übertrifft clearRect in Sachen Leistung in der Regel die zweite Variante, in Chrome 14 ist in einigen Fällen die Verwendung des Hacks canvas.width zum Zurücksetzen jedoch wesentlich schneller (jsperf):

Vorsicht bei diesem Tipp: Dieser ist stark von der zugrunde liegenden Canvas-Implementierung abhängig und unterliegt Änderungen. Weitere Informationen finden Sie im Artikel von Simon Sarris zum Löschen des Canvas.

Gleitkommakoordinaten vermeiden

Das HTML5-Canvas unterstützt Subpixel-Rendering, das sich nicht deaktivieren lässt. Wenn Sie beim Zeichnen Koordinaten verwenden, die keine ganzen Zahlen sind, wird mithilfe von Antialiasing automatisch versucht, die Linien zu glätten. Hier ist der visuelle Effekt, den ich aus diesem Artikel zur Subpixel-Canvas-Leistung von Seb Lee-Delisle entnommen habe:

Subpixel

Wenn das geglättete Sprite nicht gewünscht ist, können Sie schneller zum Erfolg kommen, indem Sie Ihre Koordinaten mit Math.floor oder Math.round in ganze Zahlen konvertieren (jsperf):

Um Ihre Gleitkommakoordinaten in ganze Zahlen zu konvertieren, kommen mehrere Techniken infrage. Bei der erfolgversprechendsten addieren Sie die Hälfte zur Zielzahl hinzu und nähern sich dann Bit für Bit an, um die Bruchzahl 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):

Beachten Sie, dass diese Art der Optimierung keine Vorteile mehr mit sich bringen sollte, sobald Canvas-Implementierungen GPU-beschleunigt sind, da in diesem Fall Gleitkommazahlen schneller gerendert werden können.

Animationen mit "requestAnimationFrame" optimieren

Das relativ neue requestAnimationFrame-API ist eine empfehlenswerte Möglichkeit zur Implementierung interaktiver Anwendungen in den Browser. Anstatt dem Browser eine bestimmte feste Tickrate zum Rendern vorzugeben, bitten Sie den Browser freundlich, Ihre Rendering-Routine aufzurufen, und werden benachrichtigt, sobald der Browser verfügbar ist. Als positiver Nebeneffekt rendert der Browser nur, wenn sich die Seite im Vordergrund befindet.

Die angestrebte Rückrufrate bei einem requestAnimationFrame-Rückruf liegt bei 60 Frames pro Sekunde, dies wird jedoch nicht garantiert, Sie müssen also nachverfolgen, wie viel Zeit seit dem letzten Rendern vergangen ist. Dies könnte folgendermaßen aussehen:

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

Beachten Sie, dass eine solche Verwendung von requestAnimationFrame auf Canvas- und andere Rendering-Technologien wie WebGL gleichermaßen anwendbar ist.

Zum Zeitpunkt der Entstehung dieses Artikels war das API nur für Chrome, Safari und Firefox verfügbar, Sie sollten also diesen Shim verwenden.

Die Mehrzahl der mobilen Canvas-Implementierungen ist langsam

Kommen wir nun zum mobilen Bereich. Zum Zeitpunkt der Entstehung dieses Artikels verfügte leider nur die Beta-Version von iOS 5.0 mit Safari 5.1 über eine GPU-beschleunigte mobile Canvas-Implementierung. Ohne die GPU-Beschleunigung verfügen die mobilen Browser in der Regel nicht über ausreichend leistungsstarke Prozessoren für moderne canvasbasierte Anwendungen. Bei einer Reihe der oben genannten JSPerf-Tests ist die Leistung im mobilen Bereich im Vergleich zu Desktops um eine Größenordnung schlechter. Dies schränkt die Auswahl an geräteübergreifenden Apps, die erfolgreich ausgeführt werden können, bedeutend ein.

Fazit

In diesem Artikel wurde eine umfassende Auswahl hilfreicher Optimierungstechniken behandelt, mit deren Hilfe sich leistungsstarke Projekte auf der Basis des HTML5-Canvas entwickeln lassen. Setzen Sie das neu Gelernte ein, um Ihre tollen Kreationen zu optimieren. Falls Sie zurzeit kein Spiel oder keine Anwendung haben, das bzw. die optimiert werden kann, lassen Sie sich von Chrome Experiments und Creative JS inspirieren.

Referenzen

Comments

0