Poprawianie wydajności kanw HTML5

Wstęp

Element canvas HTML5, który powstał w ramach eksperymentu firmy Apple, to najpowszechniej obsługiwany standard w przypadku grafiki w trybie natychmiastowym 2D w internecie. Wielu programistów używa jej do tworzenia różnorodnych projektów multimedialnych, wizualizacji i gier. Jednak wraz ze wzrostem złożoności opracowanych przez nas aplikacji deweloperzy nieumyślnie natykają się na granicę wydajności. Istnieje wiele niepowiązanych informacji na temat optymalizacji wydajności obszaru roboczego. W tym artykule chcemy skonsolidować niektóre z tych materiałów w bardziej przystępne materiały dla programistów. W tym artykule znajdziesz podstawowe optymalizacje dotyczące wszystkich środowisk grafiki komputerowej oraz techniki typowe dla Canvas, które mogą się zmieniać w miarę ulepszania implementacji Canvas. W szczególności, ponieważ dostawcy przeglądarek implementują akcelerację GPU Canvas, niektóre z omówionych technik wydajności prawdopodobnie nie będą miały większego wpływu. W odpowiednich przypadkach zostanie to odnotowane. Pamiętaj, że ten artykuł nie dotyczy korzystania z kanw HTML5. Więcej informacji znajdziesz w artykułach na temat kanw na stronie HTML5Rocks. W tym rozdziałie na stronie Dive into HTML5 oraz w MDN Canvas.

Testowanie wydajności

Aby sprostać szybko zmieniającemu się środowisku kanw HTML5, testy w JSPerf (jsperf.com) sprawdzają, czy każda proponowana optymalizacja nadal działa. JSPerf to aplikacja internetowa, która umożliwia programistom pisanie testów wydajności JavaScript. Każdy test koncentruje się na wyniku, który chcesz uzyskać (np. wyczyszczeniu obszaru roboczego), obejmuje kilka metod pozwalających osiągnąć ten sam wynik. JSPerf wykonuje każde podejście tak często, jak to możliwe w krótkim czasie, i zapewnia statystycznie istotną liczbę iteracji na sekundę. Wyższe wyniki są zawsze lepsze! Użytkownicy strony testu wydajności JSPerf mogą uruchomić test w przeglądarce i zezwolić JSPerf na zapisanie znormalizowanych wyników testu w usłudze Browserscope (browserscope.org). Ponieważ techniki optymalizacji opisane w tym artykule są poparte wynikiem JSPerf, możesz wrócić do aktualnych informacji o tym, czy dana metoda nadal jest stosowana. Napisałam małą aplikację pomocniczą, która renderuje te wyniki w postaci wykresów umieszczonych w tym artykule.

Wszystkie wyniki dotyczące wydajności opisane w tym artykule zależą od wersji przeglądarki. Okazało się to tym ograniczeniem, ponieważ nie wiemy, w jakim systemie operacyjnym działała przeglądarka, a co ważniejsze, czy wersja HTML5 była z akceleracją sprzętową podczas testu wydajności. Aby sprawdzić, czy obiekt canvas HTML5 w Chrome korzysta z akceleracji sprzętowej, otwórz na pasku adresu about:gpu.

Renderowanie wstępne na płótnie poza ekranem

Jeśli ponownie rysujesz podobne obiekty podstawowe na ekranie na wielu klatkach, co często ma miejsce w przypadku gry, możesz znacznie poprawić wydajność, wstępnie renderując duże części sceny. Renderowanie wstępne polega na korzystaniu z oddzielnego obszaru roboczego (czyli obszaru roboczego) poza ekranem, na którym renderuje się obrazy tymczasowe, a następnie renderuje obrazy niewidoczne na ekranie. Załóżmy na przykład, że chcesz ponownie rysować Mario z szybkością 60 klatek na sekundę. Możesz tu narysować kapelusz, wąsy i literę „M” przy każdej klatki lub wstępnie wyrenderować Mario przed uruchomieniem animacji. bez 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 zastosowanie requestAnimationFrame, które zostało szczegółowo omówione w dalszej sekcji.

Ta technika jest szczególnie skuteczna, jeśli proces renderowania (w tym przykładzie drawMario) jest kosztowny. Dobrym przykładem jest renderowanie tekstu, które jest bardzo kosztowne.

Słabe wyniki w przypadku „wstępnie wyrenderowanego luźnego” przypadku testowego. Podczas renderowania wstępnego musisz upewnić się, że tymczasowy obszar roboczy ściśle przylega do rysowanego obrazu. W przeciwnym razie wzrost wydajności renderowania poza ekranem zostanie przeważony przez utratę wydajności wynikającej z kopiowania jednego dużego obszaru roboczego na drugą (która zależy od rozmiaru docelowego źródła). W teście ciasne płótno jest po prostu mniejsze:

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

W porównaniu z elastycznymi elementami, które zapewniają mniejszą skuteczność:

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

Zbiorcze wywołania obszaru roboczego

Rysowanie jest kosztowne, dlatego wydajniej jest wczytać maszynę stanową za pomocą długiego zestawu poleceń, a potem zrzucić je do bufora wideo.

Na przykład podczas rysowania wielu linii lepiej jest utworzyć jedną ścieżkę ze wszystkimi znajdującymi się w niej liniami i narysować ją za pomocą jednego wywołania rysowania. Innymi słowy, zamiast rysować osobne wiersze:

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 zwiększa wydajność:

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 również technologii kanw HTML5. Na przykład podczas rysowania złożonej ścieżki lepiej umieścić w ścieżce wszystkie punkty, niż renderować segmenty oddzielnie (jsperf).

W przypadku Canvas istnieje ważny wyjątek od tej reguły: jeśli podstawowe elementy do rysowania mają małe ramki ograniczające (np. linie poziome i pionowe), lepiej wyrenderować je oddzielnie (jsperf).

Unikaj niepotrzebnych zmian stanu obszaru roboczego

Element canvas HTML5 jest zaimplementowany na maszynie stanu, który śledzi np. style wypełnienia i kreski oraz poprzednie punkty, które tworzą bieżącą ścieżkę. Próbując zoptymalizować wydajność grafiki, łatwo skoncentrować się wyłącznie na renderowaniu grafiki. Manipulacja maszyną stanu może jednak zwiększyć wydajność. Jeśli np. wyrenderujesz scenę przy użyciu wielu kolorów wypełnienia, wyrenderowanie będzie tańsze według koloru niż na podstawie umieszczenia go w obszarze roboczym. Aby wyrenderować wzór w paski, można wyrenderować pasek, zmienić kolor, wyrenderować następny 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 potem pasy 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 z przeplotem jest wolniejsze, ponieważ wymiana maszyny stanu jest kosztowna.

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

Jak można się spodziewać, renderowanie mniejszej ilości treści na ekranie jest tańsze niż jej częstsze renderowanie. Jeśli występują tylko przyrostowe różnice między ujęciami, można je znacząco poprawić, pokazując im różnice. Inaczej mówiąc, zamiast wyczyścić cały ekran przed narysowaniem:

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

Śledź narysowaną ramkę ograniczającą i tylko ją wyczyść.

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

Jeśli zajmujesz się grafiką komputerową, możesz też stosować tę technikę jako „obcinanie regionów”, w której wcześniej renderowane ramkę ograniczającą jest zapisywana, a następnie czyszczona przy każdym renderowaniu. Ta technika odnosi się również do kontekstów renderowania na podstawie pikseli, co ilustruje to prezentacja o emulatorze Nintendo w języku JavaScript.

Używanie wielowarstwowych płótna do tworzenia złożonych scen

Jak już wspomnieliśmy, rysowanie dużych obrazów jest kosztowne i w miarę możliwości należy ich unikać. Oprócz korzystania z innego obszaru roboczego do renderowania poza ekranem, jak wspomniano w sekcji dotyczącej renderowania wstępnego, możemy też nakładać na siebie płótno. Używając przezroczystości w obszarze roboczym pierwszego planu, możemy polegać na tym, że GPU będzie łączyć ze sobą wersje alfa podczas renderowania. Możesz go skonfigurować w ten sposób: 2 płaszczyzny robocze umieszczone jedna nad drugą.

<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ą tej przestrzeni jest to, że gdy rysujemy lub usuwamy tło, nigdy nie zmieniamy tła. Jeśli grę lub aplikację multimedialną możesz podzielić na pierwszy plan i w tle, rozważ renderowanie ich na osobnych obszarach roboczych, aby uzyskać znaczny wzrost wydajności.

Często możesz wykorzystać niedoskonałości ludzkiego postrzegania i wyrenderować tło tylko raz lub z mniejszą prędkością niż na pierwszym planie (co prawdopodobnie zajmie większość uwagi użytkownika). Na przykład przy każdym renderowaniu można renderować pierwszy plan, ale tło renderować tylko co n-tą klatkę. Pamiętaj też, że to podejście uogólnia się w przypadku dowolnej liczby złożonych obszarów roboczych, jeśli Twoja aplikacja działa lepiej z tego rodzaju strukturą.

Unikaj rozmycia cieni

Podobnie jak wiele innych środowisk graficznych, obiekt canvas HTML5 pozwala programistom zamazywać obiekty podstawowe, 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);

Sposoby czyszczenia obszaru roboczego

Ponieważ obiekt canvas HTML5 to paradygmat rysowania w trybie natychmiastowym, scena musi zostać przerysowana ponownie przy każdej klatce. Z tego powodu czyszczenie obszaru roboczego jest niezwykle ważne w przypadku aplikacji i gier HTML5. Jak wspomnieliśmy w sekcji Unikaj zmian stanu obszaru roboczego, czyszczenie całego obszaru roboczego jest często niepożądane, ale jeśli musisz to zrobić, możesz to zrobić na 2 sposoby: wywołaj context.clearRect(0, 0, width, height) lub użyj w tym celu mechanizmu hakerskiego dla obszaru roboczego: canvas.width = canvas.width. W momencie tworzenia tego pliku clearRect zwykle osiąga lepsze wyniki niż wersja resetowana, ale w niektórych przypadkach użycie funkcji canvas.width resetowania jest znacznie szybsze w Chrome 14.

Ostrożnie z tą wskazówką, ponieważ zależy ona w dużym stopniu od implementacji płótna i w dużym stopniu może się zmienić. Więcej informacji znajdziesz w artykule Simona Sarrisa na temat czyszczenia przestrzeni roboczej.

Unikaj współrzędnych zmiennoprzecinkowych

Kanwy HTML5 obsługuje renderowanie subpikseli i nie można go wyłączyć. Jeśli rysujesz współrzędne, które nie są liczbami całkowitymi, automatycznie używany jest antyaliasing, który próbuje wygładzić linie. Oto efekt wizualny zaczerpnięty z tego artykułu na temat skuteczności płótna z podzielonymi pikselami, który napisał Seb Lee-Delisle:

Podpiksel

Jeśli wygładzony sprite nie jest efektem, którego szukasz, możesz znacznie szybciej przekonwertować współrzędne na liczby całkowite za pomocą funkcji Math.floor lub Math.round (jsperf):

Aby przekształcić współrzędne zmiennoprzecinkowe na liczby całkowite, możesz skorzystać z kilku sprytnych technik. Najskuteczniejsze z nich to dodanie połowy wartości docelowej do wartości docelowej, a następnie wykonanie operacji bitowych 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łne zestawienie skuteczności znajdziesz tutaj (jsperf).

Pamiętaj, że ten rodzaj optymalizacji nie powinien już mieć znaczenia, gdy implementacje kanw są przyspieszane za pomocą GPU, co umożliwia szybkie renderowanie współrzędnych niebędących liczbami całkowitymi.

Optymalizuj animacje za pomocą usługi requestAnimationFrame

Względnie nowy interfejs API requestAnimationFrame to zalecany sposób implementacji interaktywnych aplikacji w przeglądarce. Zamiast nakazać przeglądarce renderowanie ze stałą częstotliwością, możesz uprzejmie poprosić ją o wywołanie rutyny renderowania, która zostanie wywołana, gdy przeglądarka będzie dostępna. Jeśli strona nie znajduje się na pierwszym planie, to znaczy, że przeglądarka jest na tyle inteligentna, że nie może jej wyświetlić. Wywołanie zwrotne requestAnimationFrame dąży do uzyskania częstotliwości wywołań zwrotnych na poziomie 60 FPS, ale nie gwarantuje tego, więc musisz śledzić czas 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 używanie requestAnimationFrame dotyczy obiektów canvas, a także innych technologii renderowania, np. WebGL. W tej chwili ten interfejs API jest dostępny tylko w Chrome, Safari i Firefoksie, więc należy użyć tej podkładki.

Większość implementacji obiektów canvas na urządzeniach mobilnych działa wolno

Porozmawiajmy o urządzeniach mobilnych. Niestety w momencie tworzenia tego tekstu tylko przeglądarka Safari w wersji beta 5.0 w systemie iOS 5.0 obsługuje wdrożenie mobilnego obszaru roboczego z akceleracją GPU. Bez akceleracji GPU przeglądarki mobilne nie mają zazwyczaj wystarczająco wydajnych procesorów do obsługi nowoczesnych aplikacji opartych na Canvas. Niektóre z opisanych powyżej testów JSPerf działają znacznie gorzej na urządzeniach mobilnych niż na komputerach, co znacznie ogranicza rodzaje aplikacji działających na różnych urządzeniach, które powinny działać.

Podsumowanie

Podsumowując, w tym artykule znajdziesz kompleksowy zestaw przydatnych technik optymalizacji, które pomogą Ci w tworzeniu wydajnych projektów opartych na kanwach HTML5. Teraz, gdy wiesz już coś nowego, zoptymalizuj swoje niesamowite kompozycje. Jeśli nie masz jeszcze gry lub aplikacji, którą można zoptymalizować, poszukaj inspiracji na stronach Eksperymenty Chrome i Creative JS.

Odniesienia