Poprawianie wydajności kanw HTML5

Wprowadzenie

Płótno HTML5, które powstało jako eksperyment firmy Apple, jest najczęściej używanym standardem grafiki w trybie natychmiastowym w internecie. Wielu deweloperów korzysta z niego w różnych projektach multimedialnych, wizualizacjach i grach. Jednak w miarę zwiększania złożoności tworzonych przez nas aplikacji deweloperzy nieumyślnie wpadają w pułapkę wydajności. Jest wiele różnych porad na temat optymalizacji wydajności kanw. Z tego artykułu deweloperzy mogą się dowiedzieć więcej o tych funkcjach w prostszy sposób. Z tego artykułu dowiesz się, jak optymalizować obrazy w ramach podstawowych opcji dostępnych we wszystkich środowiskach graficznych, a także technik związanych z danym typem obrazu, które mogą się zmieniać wraz z ulepszaniem implementacji. W szczególności, gdy dostawcy przeglądarek wprowadzą przyspieszenie GPU w Canvas, niektóre opisane techniki dotyczące wydajności prawdopodobnie będą miały mniejszy wpływ. W odpowiednich miejscach zostanie to zaznaczone. Pamiętaj, że ten artykuł nie dotyczy korzystania z tapety HTML5. W tym celu przeczytaj te artykuły na temat kanwy na stronie HTML5Rocks, rozdział na stronie Zagłębianie HTML5 lub samouczek MDN Canvas.

Testy wydajności

Aby uwzględnić szybko zmieniający się świat tła HTML5, testy JSPerf (jsperf.com) sprawdzają, czy każda zaproponowana optymalizacja nadal działa. JSPerf to aplikacja internetowa, która umożliwia deweloperom pisanie testów wydajności JavaScript. Każdy test skupia się na wyniku, który chcesz osiągnąć (np. czyszczenie obszaru roboczego), i obejmuje kilka podejść, które pozwalają osiągnąć ten sam wynik. JSPerf wykonuje każdą metodę tyle razy, ile to możliwe, w krótkim czasie i podaje statystycznie istotną liczbę iteracji na sekundę. Im wyższy wynik, tym lepiej. Użytkownicy, którzy wejdą na stronę testu wydajności JSPerf, mogą przeprowadzić test w swojej przeglądarce i zezwolić JSPerf na zapisanie znormalizowanych wyników w Browserscope (browserscope.org). Techniki optymalizacji opisane w tym artykule są poparte wynikami JSPerf, więc możesz do nich wrócić, aby uzyskać aktualne informacje o tym, czy dana technika jest nadal aktualna. Napisałem do tego artykułu małą aplikację pomocniczą, która renderuje wyniki w postaci wykresów.

Wszystkie wyniki dotyczące wydajności w tym artykule są powiązane z wersją przeglądarki. Okazuje się, że jest to ograniczenie, ponieważ nie wiemy, na jakim systemie operacyjnym działała przeglądarka ani, co ważniejsze, czy podczas testu wydajności canvas HTML5 był przyspieszany sprzętowo. Aby sprawdzić, czy kanwa HTML5 w Chrome jest przyspieszana sprzętowo, wpisz about:gpu w pasku adresu.

Wstępne renderowanie na płótnie poza ekranem

Jeśli na ekranie ponownie rysujesz podobne prymitywy w wielu klatkach, co często ma miejsce podczas pisania gry, możesz znacznie poprawić wydajność, wstępnie renderując duże części sceny. Prerenderowanie polega na użyciu osobnej, niewidocznej siatki (lub siatek), na której renderowane są tymczasowe obrazy, a następnie renderowanie siatek z powrotem na widoczną siatkę. Załóżmy np., że ponownie rysujesz Mario z szybkością 60 klatek na sekundę. Możesz narysować kapelusz, wąsy i literę „M” na każdym klatce lub wyrenderować Mario przed uruchomieniem animacji. brak renderowania wstępnego:

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

renderowanie wstępne:

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

Zwróć uwagę na użycie właściwości requestAnimationFrame, które zostało szczegółowo omówione w dalszej sekcji.

Ta technika jest szczególnie skuteczna, gdy operacja renderowania (drawMario w przypadku podanego powyżej przykładu) jest droga. Dobrym przykładem jest tutaj renderowanie tekstu, które jest bardzo kosztowną operacją.

Jednak słaba wydajność przypadku testowego „wstępnie wyrenderowany luźno”. Podczas wstępnego renderowania ważne jest, aby tymczasowe płótno ściśle przylegało do obrazu, który rysujesz. W przeciwnym razie wzrost wydajności dzięki renderowaniu poza ekranem będzie równoważony utratą wydajności podczas kopiowania jednego dużego płótna na drugie (co zależy od rozmiaru docelowego źródła). W przypadku ciasnego obrazu w powyższym teście jest on po prostu mniejszy:

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

W porównaniu do grupy luźnej, która daje niższą skuteczność:

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

Zbiorcze wywołania obszaru roboczego

Rysowanie jest kosztowną operacją, dlatego wydajniej jest wczytać maszynę stanów rysowania za pomocą długiego zestawu poleceń, a potem zrzucać je wszystkie do bufora wideo.

Na przykład przy rysowaniu wielu linii lepiej jest utworzyć jedną ścieżkę ze wszystkimi liniami i narysować ją za pomocą jednego wywołania. Innymi słowy, zamiast rysować oddzielne linie:

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

Rysowanie jednej linii łamanej przynosi lepsze wyniki:

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

Dotyczy to też świata HTML5 canvas. Na przykład przy rysowaniu ścieżki złożonej lepiej umieścić w niej wszystkie punkty, zamiast renderować segmenty oddzielnie (jsperf).

Pamiętaj jednak, że w przypadku Canvas istnieje ważne odstępstwo od tej reguły: jeśli prymitywy używane do rysowania żądanego obiektu mają małe ramki ograniczające (np. poziome i pionowe linie), renderowanie ich osobno może być bardziej wydajne (jsperf).

Unikanie niepotrzebnych zmian stanu obszaru roboczego

Element HTML5 canvas jest implementowany na maszynie stanów, która śledzi takie elementy, jak style wypełnienia i obrysowania, a także poprzednie punkty, które tworzą bieżącą ścieżkę. Podczas optymalizacji wydajności grafiki łatwo jest skupić się wyłącznie na jej renderowaniu. Manipulowanie maszyną stanów może jednak powodować wzrost obciążenia związanego z wydajnością. Jeśli do renderowania sceny używasz wielu kolorów wypełnienia, renderowanie według koloru jest tańsze niż według położenia na płótnie. Aby wyrenderować wzór pasków, możesz najpierw wyrenderować pasek, zmienić kolory i wyrenderować kolejny pasek itd.:

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

Lub wyrenderuj wszystkie nieparzyste paski, a następnie wszystkie paski parzyste:

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

Zgodnie z oczekiwaniami podejście mieszane jest wolniejsze, ponieważ zmiana maszyny stanów jest kosztowna.

Renderuj tylko różnice na ekranie, a nie nowy stan

Jak można się spodziewać, renderowanie mniejszej ilości elementów na ekranie jest tańsze niż renderowanie większej ilości. Jeśli różnice między poszczególnymi odświeżeniami są tylko przyrostowe, możesz znacznie zwiększyć wydajność, rysując tylko tę różnicę. Innymi słowy, zamiast czyszczenia całego ekranu przed narysowaniem:

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

Pamiętaj o zaznaczonej ramce ograniczającej i usuń ją.

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

Jeśli znasz się na grafice komputerowej, być może znasz tę technikę jako „ponowną rysowanie regionów”, w której wcześniej wyrenderowany prostokąt ograniczający jest zapisywany, a następnie oczyszczany przy każdym renderowaniu. Ta technika stosuje się również w kontekstach renderowania opartych na pikselach, co ilustruje prezentacja o emulatorze Nintendo w języku JavaScript.

Używanie wielu warstw na płótnie w przypadku złożonych scen

Jak już wspomnieliśmy, renderowanie dużych obrazów jest kosztowne i należy go unikać, jeśli to możliwe. Oprócz użycia innego płótna do renderowania ekranu wyłączonego, jak pokazano w sekcji wstępnego renderowania, możemy też użyć nałożonych na siebie płót. Dzięki zastosowaniu przezroczystości w obszarze tła możemy polegać na GPU, który podczas renderowania będzie łączyć warstwy alfa. Możesz to skonfigurować w ten sposób, aby 2 płótna były umieszczone jedno na drugim.

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

Zaletą tego rozwiązania w porównaniu z jednym płótnem jest to, że gdy rysujemy lub czyścimy tło, nie modyfikujemy tła. Jeśli Twoja gra lub aplikacja multimedialna może być podzielona na pierwszy i drugi plan, rozważ renderowanie ich na osobnych obszarach roboczych, aby uzyskać znaczny wzrost wydajności.

Często zdarza się, że wykorzystujesz niedoskonałości tego, z czego korzystasz, i renderujesz tło tylko raz lub z mniejszą prędkością niż na pierwszym planie (co prawdopodobnie zajmuje większość jego uwagi). Możesz na przykład renderować pierwszy plan za każdym razem, gdy renderujesz obraz, ale tło tylko co N klatek. Pamiętaj też, że to podejście sprawdza się w przypadku dowolnej liczby złożonych kanałów, jeśli Twoja aplikacja działa lepiej z takiego rodzaju strukturą.

Unikaj funkcji shadowBlur

Podobnie jak wiele innych środowisk graficznych, kanwa HTML5 umożliwia deweloperom rozmycie prymitywów, ale ta operacja może być bardzo kosztowna:

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

Poznaj różne sposoby czyszczenia płótna

Ponieważ kanwa HTML5 jest paradygmatem rysowania w trybie natychmiastowym, scena musi być ponownie rysowana w każdej klatce. Dlatego czyszczenie kanwy jest bardzo ważną operacją w przypadku aplikacji i gier korzystających z kanwy HTML5. Jak wspomniano w sekcji Unikanie zmian stanu płótna, czyszczenie całego płótna jest często niepożądane, ale jeśli musisz to zrobić, masz 2 opcje: wywołanie funkcji context.clearRect(0, 0, width, height) lub użycie hacka dla danego typu płótna: canvas.width = canvas.width. W momencie pisania tego artykułu funkcja clearRect zazwyczaj działa lepiej niż wersja z resetem szerokości, ale w niektórych przypadkach użycie hacka canvas.width do resetowania jest znacznie szybsze w Chrome 14.

Stosuj tę wskazówkę z uwagą, ponieważ zależy ona w dużej mierze od implementacji canvasa i może ulec zmianie. Więcej informacji znajdziesz w artykule Simona Sarrisa o czyszczeniu płótna.

Unikaj współrzędnych zmiennoprzecinkowych

Płótno HTML5 obsługuje renderowanie subpikselowe i nie ma możliwości wyłączenia tej funkcji. Jeśli rysujesz za pomocą współrzędnych, które nie są liczbami całkowitymi, automatycznie używa ono wygładzania krawędzi, aby wygładzić linie. Oto efekt wizualny pochodzący z tego artykułu Seb Lee-Delisle o wydajności canvasa w subpikselach:

Poniżej piksela

Jeśli nie potrzebujesz wygładzonego sprite’a, możesz znacznie szybciej przekształcić współrzędne na liczby całkowite za pomocą funkcji Math.floor lub Math.round (jsperf):

Aby przekonwertować współrzędne zmiennoprzecinkowe na liczby całkowite, możesz zastosować kilka sprytnych technik, z których najefektywniej można dodać połowę wartości docelowej i wykonywać operacje bitowe na wyniku w celu wyeliminowania części ułamkowej.

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

Pełny opis wydajności znajdziesz tutaj (jsperf).

Pamiętaj, że tego rodzaju optymalizacja nie powinna już mieć znaczenia, gdy implementacje kanwy zostaną akcelerowane za pomocą GPU, co może szybko renderować współrzędne niecałkowite.

Zoptymalizuj animacje w requestAnimationFrame

Interfejs API requestAnimationFrame to stosunkowo nowy interfejs, który jest zalecanym sposobem implementowania aplikacji interaktywnych w przeglądarce. Zamiast nakazywać przeglądarce renderowanie z określoną stałą szybkością osi czasu, uprzejmie prosimy ją o wywołanie procedury renderowania, które jest wykonywane, gdy przeglądarka jest dostępna. Dobrym efektem ubocznym jest to, że jeśli strona nie znajduje się na pierwszym planie, przeglądarka jest na tyle inteligentna, że nie może się wyrenderować. Wywołanie zwrotne requestAnimationFrame ma na celu częstotliwość wywołań zwrotnych 60 FPS, ale tego nie gwarantuje, dlatego musisz śledzić, ile czasu upłynęło od ostatniego renderowania. Może to wyglądać mniej więcej tak:

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

Pamiętaj, że to użycie requestAnimationFrame dotyczy zarówno canvasa, jak i innych technologii renderowania, takich jak WebGL. W momencie pisania tego artykułu interfejs API jest dostępny tylko w Chrome, Safari i Firefox, więc należy użyć tego shimu.

Większość implementacji obszaru roboczego na urządzeniach mobilnych działa wolno

Porozmawiajmy o urządzeniach mobilnych. W momencie pisania tego artykułu tylko system iOS 5.0 w wersji beta z Safari 5.1 ma implementację mobilnego kanału wejściowego z przyspieszeniem sprzętowym. Bez akceleracji GPU procesory w przeglądarkach mobilnych zwykle nie są wystarczająco wydajne do obsługi nowoczesnych aplikacji opartych na płótnie. Niektóre z opisanych powyżej testów JSPerf działają na urządzeniach mobilnych o wiele gorzej niż na komputerach, co znacznie ogranicza rodzaje aplikacji wieloplatformowych, które mogą działać prawidłowo.

Podsumowanie

Podsumowując, w tym artykule omówiliśmy kompleksowy zestaw przydatnych technik optymalizacji, które pomogą Ci tworzyć wydajne projekty na kanwie HTML5. Teraz, gdy wiesz już coś nowego, możesz zoptymalizować swoje niesamowite kompozycje. Jeśli nie masz obecnie gry ani aplikacji, którą trzeba zoptymalizować, zaczerpnij inspiracji z eksperymentów w ChromeCreative JS.

Pliki referencyjne