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

Борис Смус
Boris Smus

Введение

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 для вдохновения.

Ссылки

,

Борис Смус
Boris Smus

Введение

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 для вдохновения.

Ссылки

,

Борис Смус
Boris Smus

Введение

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 -й кадр. Также обратите внимание, что этот подход хорошо обобщает любое количество композитных полосов, если ваше приложение работает лучше с такой структурой.

Избегайте Shadowblur

Как и многие другие графические среды, 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 14

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

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

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

Суб-пиксель

Если сглаженный спрайт не является тем, что вы ищете, может быть гораздо быстрее преобразовать ваши координаты в целые числа, используя 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 FPS, но не гарантирует его, поэтому вам нужно отслеживать, сколько времени прошло с момента последнего визуализации. Это может выглядеть как -то вроде следующего:

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, поэтому вы должны использовать этот Shim .

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

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

Заключение

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

Ссылки