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 tego formatu 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. Istnieje wiele niespójności w informacjach dotyczących optymalizacji wydajności obrazu. 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 staną się mniej skuteczne. 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 Canvas na stronie HTML5Rocks, rozdział na stronie Zagłębianie HTML5 lub samouczek Canvas w MDN.
Testy wydajności
Aby uwzględnić szybko zmieniający się świat HTML5 canvas, 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 koncentruje się na wyniku, który chcesz uzyskać (np. wyczyszczeniu płótna), i zawiera kilka metod, które prowadzą do tego samego wyniku. 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 małą aplikację pomocniczą, która wyświetla te wyniki w postaci wykresów. Zostały one umieszczone w tym artykule.
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 zwiększyć wydajność, wstępnie renderując duże części sceny. Prerenderowanie oznacza użycie osobnego off-screen canvas (lub wielu off-screen canvasów), na których renderowane są tymczasowe obrazy, a następnie renderowanie off-screen canvasów z powrotem na widoczny ekran. Załóżmy na przykład, że rysujesz postać Mario biegnącą z prędkością 60 klatek na sekundę. Możesz narysować kapelusz, wąsy i literę „M” na każdym klatce lub wyrenderować Mario przed uruchomieniem animacji. bez prerenderowania:
// 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 requestAnimationFrame
, które jest omawiane bardziej szczegółowo w następnej 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 spadkiem wydajności spowodowanym kopiowaniem 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 z luźną, która zapewnia gorszą wydajność:
can3.width = 300;
can3.height = 100;
Wykonywanie zbiorczych wywołań obszaru roboczego
Rysowanie jest kosztowną operacją, dlatego wydajniej jest wczytać maszynę stanów rysowania za pomocą długiego zestawu poleceń, a następnie zrzucać je wszystkie do bufora wideo.
Na przykład podczas rysowania wielu linii lepiej jest utworzyć jedną ścieżkę z wszystkimi liniami i narysować ją za pomocą jednego wywołania funkcji draw(). Inaczej mówiąc, 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();
}
Lepszą skuteczność uzyskujemy dzięki rysowaniu pojedynczej ścieżki wielopunktowej:
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. Podczas rysowania złożonej ścieżki lepiej jest umieścić wszystkie punkty na ścieżce, zamiast renderować poszczególne segmenty osobno (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ć wydajniejsze (jsperf).
Unikaj niepotrzebnych zmian stanu kanwy
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, z których składa się bieżąca ścieżka. Podczas optymalizacji wydajności grafiki łatwo jest skupić się wyłącznie na jej renderowaniu. Manipulowanie maszyną stanów może jednak również 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);
}
Możesz też renderować najpierw wszystkie nieparzyste paski, a potem 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.
renderowanie tylko różnic na ekranie, a nie całego nowego stanu;
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);
Śledź narysowaną ramkę ograniczającą i usuń tylko ją.
context.fillRect(last.x, last.y, last.width, last.height);
Jeśli znasz się na grafice komputerowej, możesz też znać 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 dotyczy też kontekstów renderowania opartych na pikselach, co ilustruje ten JavaScriptowy emulator Nintendo.
Używanie wielu warstw w 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ć znaczną poprawę wydajności.
Często możesz wykorzystać niedoskonałe ludzkie postrzeganie i renderować tło tylko raz lub z mniejszymi prędkościami niż w przypadku pierwszego planu (który prawdopodobnie przyciąga najwięcej uwagi użytkownika). 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 zastrzeżeniem, 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 czyszczaniu 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:

Jeśli wygładzony sprite nie jest efektem, którego szukasz, możesz znacznie szybciej przekształcić współrzędne na liczby całkowite, używając funkcji Math.floor
lub Math.round
(jsperf):
Aby przekształcić współrzędne zmiennoprzecinkowe na liczby całkowite, możesz użyć kilku sprytnych technik. Najskuteczniejsze z nich polegają na dodawaniu połowy do liczby docelowej, a następnie wykonywaniu operacji bitowych na wyniku, aby wyeliminować część ułamkową.
// 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 typu optymalizacja nie będzie już potrzebna, gdy implementacje canvas będą przyspieszane przez GPU, co pozwoli na szybkie renderowanie współrzędnych niebędących liczbami całkowitymi.
Optymalizacja animacji za pomocą requestAnimationFrame
Interfejs API requestAnimationFrame
to stosunkowo nowy interfejs, który jest zalecanym sposobem implementowania aplikacji interaktywnych w przeglądarce. Zamiast wydawać przeglądarce polecenie renderowania z określoną stałą częstotliwością odświeżania, możesz grzecznie poprosić ją o wywołanie procedury renderowania i wywołanie tej procedury, gdy przeglądarka będzie dostępna. Jeśli strona nie jest na pierwszym planie, przeglądarka jest na tyle inteligentna, że nie będzie jej renderować.
W przypadku wywołania zwrotnego requestAnimationFrame
docelowa liczba klatek na sekundę wynosi 60 FPS, ale nie jest to gwarantowane, dlatego 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 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 mobilnych canvas jest powolna
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 już wiesz, jak to zrobić, możesz optymalizować swoje niesamowite kreacje. Jeśli nie masz obecnie gry ani aplikacji do optymalizacji, zainspiruj się eksperymentami w Chrome i Creative JS.
Pliki referencyjne
- Tryb Natychmiastowy a tryb zachowaj.
- Inne artykuły dotyczące HTML5Rocks
- Sekcja Canvas w artykule Zagłębianie się w HTML5.
- JSPerf umożliwia deweloperom tworzenie testów wydajności kodu JS.
- Browserscope przechowuje dane o wydajności przeglądarki.
- JSPerfView, który renderuje testy JSPerf jako wykresy.
- Post na blogu Simona na temat czyszczenia obszaru roboczego oraz jego książka HTML5 Unleashed, która zawiera rozdziały o skuteczności obszaru roboczego.
- Post na blogu Sebastiana na temat wydajności renderowania poniżej piksela.
- Prezentacja Bena na temat optymalizacji emulatora NES w JS.
- nowy profil narzędzia do analizowania działania odbitek w Narzędziach deweloperskich w Chrome,