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. Wenn die von uns entwickelten Anwendungen jedoch immer komplexer werden, stoßen Entwickler ungewollt an die Leistungsgrenze. 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 noch 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, die zum gleichen Ergebnis führen. 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 in 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 rendert, die in diesen Artikel eingebettet ist.
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 rendern als Canvas außerhalb des Bildschirms
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 oder mehrere separate Canvases (außerhalb des Bildschirms) zum Rendern temporärer Bilder verwendet. Anschließend werden diese Canvases wieder auf das sichtbare Canvas übertragen. 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 näher 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 mit dem Status „Pre-Rendering Loose“ ist jedoch schlecht. 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 ausgeglichen, was von der Größe des Quell- und Zielobjekts abhängt. Ein passendes Canvas im obigen Test ist einfach kleiner:
can2.width = 100;
can2.height = 40;
Im Vergleich zu der Methode mit dem lockeren Effekt, mit der sich die Leistung schlechter erzielen lässt:
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).
Beachten Sie jedoch, dass es bei Canvas eine wichtige Ausnahme von dieser Regel gibt: Wenn die zum Zeichnen des gewünschten Objekts verwendeten Primitive 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 ü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 Zustandsautomat kann jedoch auch zu einem Leistungseinbußen führen. 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 und 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 Zeilensprung-Ansatz langsamer, da das Ändern des Zustandsautomaten teuer ist.
Nur Bildschirmunterschiede rendern, nicht der gesamte neue Zustand
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 Rendering 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 deutliche 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 viele andere Grafikumgebungen ermöglicht auch HTML5 Canvas Entwicklern, Primitive zu verwischen. 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-Zustandsä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 bei diesem Tipp vorsichtig, da er stark von der zugrunde liegenden Canvas-Implementierung abhängt und sich stark ä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:
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).
Beachten Sie, dass diese Art der Optimierung keine Rolle mehr spielen sollte, sobald Canvas-Implementierungen GPU-beschleunigt sind, sodass Koordinaten ohne Ganzzahl 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 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 Dokuments ist diese API nur in Chrome, Safari und Firefox verfügbar, daher sollten Sie dieses Shim verwenden.
Die meisten Canvas-Implementierungen für Mobilgeräte sind langsam
Sprechen wir über Mobilgeräte. Leider verfügte zum Zeitpunkt der Entstehung dieses Artikels nur die Betaversion von iOS 5.0 mit Safari 5.1 über eine GPU-beschleunigte mobile Canvas-Implementierung. Ohne GPU-Beschleunigung haben mobile Browser in der Regel nicht leistungsstark genug CPUs für moderne Canvas-basierte Anwendungen. Bei einer Reihe der oben beschriebenen JSPerf-Tests ist die Leistung auf Mobilgeräten im Vergleich zu Computern wesentlich schlechter. Dadurch werden die Arten von geräteübergreifenden Apps, die wahrscheinlich erfolgreich ausgeführt werden können, stark eingeschränkt.
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
- Sofortiger Modus im Vergleich zum beibehaltenen Modus.
- Weitere Canvas-Artikel von HTML5Rocks
- Im Canvas-Abschnitt von „Einstieg in HTML5“
- Mit JSPerf können Entwickler JS-Leistungstests erstellen.
- Browserscope speichert Browserleistungsdaten.
- JSPerfView rendert JSPerf-Tests als Diagramme.
- Simons Blogpost zum Löschen des Canvas und sein Buch HTML5 Unleashed mit Kapiteln zur Canvas-Leistung
- Sebastians Blogpost zur Leistung von Subpixel-Rendering
- Vortrag von Ben zur Optimierung eines JS NES-Emulators.
- Der neue Canvas-Profiler in den Chrome-Entwicklertools.