Улучшение производительности HTML5 Canvas

Введение

HTML5 Canvas, созданный в качестве эксперимента Apple, является наиболее широко поддерживаемым стандартом для 2D- графики в немедленном режиме в Интернете. Многие разработчики теперь полагаются на него в самых разных мультимедийных проектах, визуализациях и играх. Однако по мере того, как создаваемые нами приложения становятся все сложнее, разработчики непреднамеренно упираются в предел производительности. Существует много бессвязных мудростей об оптимизации производительности холста. Целью этой статьи является объединение некоторых частей этой статьи в более удобоваримый ресурс для разработчиков. В этой статье описаны фундаментальные оптимизации, применимые ко всем средам компьютерной графики, а также методы, специфичные для холста, которые могут быть изменены по мере совершенствования реализаций холста. В частности, поскольку производители браузеров реализуют ускорение графического процессора Canvas, некоторые из описанных выше методов повышения производительности, вероятно, станут менее эффективными. При необходимости это будет отмечено. Обратите внимание, что в этой статье не рассматривается использование холста HTML5. Для этого ознакомьтесь со статьями, посвященными холсту , на HTML5Rocks, этой главой на сайте Dive in HTML5 или MDN Canvas . руководство.

Тестирование производительности

Чтобы соответствовать быстро меняющемуся миру холста HTML5, тесты JSPerf ( jsperf.com ) проверяют, что каждая предложенная оптимизация по-прежнему работает. JSPerf — это веб-приложение, которое позволяет разработчикам писать тесты производительности JavaScript. Каждый тест ориентирован на результат, которого вы пытаетесь достичь (например, очистка холста), и включает в себя несколько подходов, позволяющих достичь одного и того же результата. JSPerf запускает каждый подход максимально возможное количество раз за короткий период времени и выдает статистически значимое количество итераций в секунду. Более высокие баллы всегда лучше! Посетители страницы теста производительности JSPerf могут запустить тест в своем браузере и позволить JSPerf сохранить нормализованные результаты теста в Browserscope ( browserscope.org ). Поскольку методы оптимизации, описанные в этой статье, подкреплены результатами JSPerf, вы можете вернуться к ним и просмотреть актуальную информацию о том, применяется ли этот метод по-прежнему. Я написал небольшое вспомогательное приложение , которое отображает эти результаты в виде графиков и встроено в эту статью.

Все результаты производительности в этой статье привязаны к версии браузера. Это оказывается ограничением, поскольку мы не знаем, на какой ОС работал браузер, и, что еще более важно, было ли аппаратное ускорение HTML5 Canvas во время выполнения теста производительности. Вы можете узнать, имеет ли холст HTML5 Chrome аппаратное ускорение, посетив about:gpu в адресной строке.

Предварительный рендеринг на холсте за кадром

Если вы перерисовываете одинаковые примитивы на экране в нескольких кадрах, как это часто бывает при написании игры, вы можете добиться значительного увеличения производительности, предварительно отрисовав большие части сцены. Предварительный рендеринг означает использование отдельного закадрового холста (или холстов) для рендеринга временных изображений, а затем рендеринг закадровых холстов обратно на видимый. Например, предположим, что вы перерисовываете Марио, бегущего со скоростью 60 кадров в секунду. Вы можете либо перерисовать его шляпу, усы и букву «М» в каждом кадре, либо выполнить предварительную визуализацию Марио перед запуском анимации. без предварительного рендеринга:

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

предварительный рендеринг:

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

Обратите внимание на использование requestAnimationFrame , которое более подробно обсуждается в следующем разделе.

Этот метод особенно эффективен, когда операция рендеринга ( drawMario в приведенном выше примере) является дорогостоящей. Хорошим примером является рендеринг текста, который является очень дорогостоящей операцией.

Однако низкая производительность «предварительно отрисованного» тестового примера. При предварительном рендеринге важно убедиться, что временный холст плотно прилегает к рисуемому изображению, иначе выигрыш в производительности от рендеринга за кадром будет уравновешен потерей производительности при копировании одного большого холста на другой (которая варьируется в зависимости от функция исходного целевого размера). Плотный холст в приведенном выше тесте просто меньше:

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

По сравнению со свободным, который дает меньшую производительность:

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

Пакетные вызовы Canvas вместе

Поскольку рисование — дорогостоящая операция, более эффективно загрузить конечный автомат рисования длинным набором команд, а затем загрузить их все в видеобуфер.

Например, при рисовании нескольких линий более эффективно создать один путь со всеми линиями в нем и нарисовать его с помощью одного вызова отрисовки. Другими словами, вместо того, чтобы рисовать отдельные линии:

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

Мы получаем лучшую производительность от рисования одной полилинии:

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

Это относится и к миру холста HTML5. Например, при рисовании сложного пути лучше поместить все точки в путь, а не отображать сегменты по отдельности ( jsperf ).

Однако обратите внимание, что в Canvas есть важное исключение из этого правила: если примитивы, участвующие в рисовании желаемого объекта, имеют небольшие ограничивающие рамки (например, горизонтальные и вертикальные линии), на самом деле может быть более эффективно визуализировать их отдельно ( jsperf ).

Избегайте ненужных изменений состояния холста

Элемент холста HTML5 реализован поверх конечного автомата, который отслеживает такие вещи, как стили заливки и обводки, а также предыдущие точки, составляющие текущий путь. При попытке оптимизировать производительность графики возникает соблазн сосредоточиться исключительно на рендеринге графики. Однако манипулирование конечным автоматом также может привести к снижению производительности. Например, если вы используете несколько цветов заливки для рендеринга сцены, дешевле рендерить по цвету, чем путем размещения на холсте. Чтобы визуализировать узор из тонких полосок, вы можете визуализировать полосу, изменить цвета, визуализировать следующую полосу и т. д.:

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

Или визуализируйте все нечетные полосы, а затем все четные:

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

Как и ожидалось, чересстрочный подход работает медленнее, поскольку изменение конечного автомата обходится дорого.

Только различия в экране рендеринга, а не все новое состояние

Как и следовало ожидать, рендеринг меньшего количества данных на экране обходится дешевле, чем рендеринг большего количества. Если между перерисовками имеются лишь незначительные различия, вы можете получить значительный прирост производительности, просто прорисовав разницу. Другими словами, вместо очистки всего экрана перед рисованием:

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

Следите за нарисованной ограничивающей рамкой и очищайте только ее.

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

Если вы знакомы с компьютерной графикой, возможно, вам известен этот метод под названием «области перерисовки», при котором ранее отрисованный ограничивающий прямоугольник сохраняется, а затем очищается при каждом рендеринге. Этот метод также применим к контекстам рендеринга на основе пикселей, как показано в этом докладе об эмуляторе JavaScript Nintendo .

Используйте несколько слоев холста для сложных сцен.

Как упоминалось ранее, рисование больших изображений обходится дорого, и его следует избегать, если это возможно. Помимо использования другого холста для рендеринга за кадром, как показано в разделе предварительной рендеринга, мы также можем использовать холсты, наложенные друг на друга. Используя прозрачность в холсте переднего плана, мы можем рассчитывать на то, что графический процессор объединит альфа-изображения во время рендеринга. Вы можете настроить это следующим образом: два абсолютно позиционированных холста один поверх другого.

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

Преимущество использования только одного холста заключается в том, что когда мы рисуем или очищаем холст переднего плана, мы никогда не изменяем фон. Если вашу игру или мультимедийное приложение можно разделить на передний и фоновый план, рассмотрите возможность их рендеринга на отдельных холстах, чтобы значительно повысить производительность.

Часто вы можете воспользоваться несовершенным человеческим восприятием и визуализировать фон только один раз или с меньшей скоростью по сравнению с передним планом (который, вероятно, будет занимать большую часть внимания вашего пользователя). Например, вы можете визуализировать передний план каждый раз при рендеринге, а фон — только в каждом N-м кадре. Также обратите внимание, что этот подход хорошо обобщается для любого количества составных холстов, если ваше приложение лучше работает с такой структурой.

Избегайте размытия теней

Как и многие другие графические среды, HTML5 Canvas позволяет разработчикам размывать примитивы, но эта операция может быть очень дорогостоящей:

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

Знать различные способы очистки холста

Поскольку холст HTML5 представляет собой парадигму рисования в немедленном режиме , сцену необходимо явно перерисовывать в каждом кадре. По этой причине очистка холста является фундаментально важной операцией для приложений и игр HTML5 Canvas. Как упоминалось в разделе «Избегайте изменений состояния холста» , очистка всего холста часто нежелательна, но если вам необходимо это сделать, есть два варианта: вызов context.clearRect(0, 0, width, height) или использование хака, специфичного для холста. чтобы это сделать: canvas.width = canvas.width ;. На момент написания clearRect обычно превосходит версию со сбросом ширины, но в некоторых случаях использование хака сброса canvas.width в Chrome 14 происходит значительно быстрее.

Будьте осторожны с этим советом, поскольку он во многом зависит от базовой реализации холста и может сильно измениться. Дополнительную информацию см. в статье Саймона Сарриса об очистке холста .

Избегайте координат с плавающей запятой

HTML5 Canvas поддерживает субпиксельный рендеринг, и отключить его невозможно. Если вы рисуете с координатами, которые не являются целыми числами, он автоматически использует сглаживание, чтобы попытаться сгладить линии. Вот визуальный эффект, взятый из статьи Себа Ли-Делисла о субпиксельных холстах :

Субпиксель

Если сглаженный спрайт не является тем эффектом, который вам нужен, гораздо быстрее можно преобразовать координаты в целые числа с помощью Math.floor или Math.round ( jsperf ):

Чтобы преобразовать координаты с плавающей запятой в целые числа, вы можете использовать несколько хитроумных методов, наиболее эффективный из которых заключается в добавлении половины к целевому числу, а затем выполнении поразрядных операций над результатом для исключения дробной части.

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

Полная информация о производительности находится здесь ( jsperf ).

Обратите внимание, что такого рода оптимизация больше не будет иметь значения, как только реализации холста будут ускорены графическим процессором, что позволит быстро отображать нецелочисленные координаты.

Оптимизируйте свою анимацию с помощью requestAnimationFrame

Относительно новый API requestAnimationFrame — рекомендуемый способ реализации интерактивных приложений в браузере. Вместо того, чтобы приказывать браузеру выполнять рендеринг с определенной фиксированной частотой, вы вежливо просите браузер вызвать вашу процедуру рендеринга и получаете вызов, когда браузер доступен. В качестве приятного побочного эффекта, если страница не на переднем плане, браузер достаточно умен, чтобы не отображать ее. Обратный вызов requestAnimationFrame нацелен на частоту обратного вызова 60 кадров в секунду, но не гарантирует ее, поэтому вам необходимо отслеживать, сколько времени прошло с момента последнего рендеринга. Это может выглядеть примерно так:

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

Обратите внимание, что такое использование requestAnimationFrame применимо к холсту, а также к другим технологиям рендеринга, таким как WebGL. На момент написания этот API доступен только в Chrome, Safari и Firefox, поэтому вам следует использовать эту прокладку .

Большинство мобильных реализаций Canvas работают медленно.

Давайте поговорим о мобильных устройствах. К сожалению, на момент написания только бета-версия iOS 5.0 с Safari 5.1 имела реализацию мобильного холста с графическим ускорением. Без ускорения графического процессора мобильные браузеры обычно не имеют достаточно мощных процессоров для современных приложений на основе холста. Ряд тестов JSPerf, описанных выше, работают на мобильных устройствах на порядок хуже, чем на настольных компьютерах, что значительно ограничивает типы приложений для разных устройств, на успешный запуск которых вы можете рассчитывать.

Заключение

Напомним, что в этой статье представлен полный набор полезных методов оптимизации, которые помогут вам разрабатывать производительные проекты на основе холста HTML5. Теперь, когда вы узнали здесь что-то новое, приступайте к оптимизации своих потрясающих творений. Или, если у вас в настоящее время нет игры или приложения для оптимизации, посмотрите Chrome Experiments и Creative JS для вдохновения.

Рекомендации