Взгляд изнутри: приложение "Пузырьки"
В предыдущей статье мы обсуждали улучшения производительности JavaScript, внедренные в Internet Explorer 10. Сегодня мы предлагаем вашему вниманию приложение Пузырьки, в основу которого положена симуляция BubbleMark (автор — Алекс Гаврилов), чтобы вы могли на деле опробовать некоторые из этих преимуществ. Текущая версия была значительно расширена, чтобы реализовать преимущества новых компонентов веб-платформы, и включает характеристики, присущие играм в формате HTML5. В этой статье мы рассмотрим ряд скрытых моментов, чтобы узнать, как составлена демонстрационная версия, и изучить основные факторы, влияющие на ее производительность.
Демонстрационная версия приложения "Пузырьки" в Internet Explorer 10 в стиле Metro в Windows 8 Release Preview
Структура демонстрации
Демонстрация состоит из некоторого числа анимированных пузырьков, парящих в пространстве. В основу положен относительно простой физический модуль JavaScript. В каждом кадре анимации, примерно 60 раз в секунду, физический модуль заново вычисляет расположение всех пузырьков, регулирует скорость каждого пузырька с учетом гравитации, а также рассчитывает эффекты столкновений. Все эти вычисления подразумевают обширные расчеты чисел с плавающей запятой. Каждый пузырек на экране представлен DOM-элементом изображения, к которому применено преобразование CSS. Сначала изображение воспроизводится в точке появления, а затем динамически масштабируется для придания эффекта надувания. В JavaScript каждый пузырек представлен как объект со свойствами доступа, как задано в ECMAScript 5.
За парящими пузырьками находится крупное изображение, которое постепенно скрывается за сплошной непрозрачной маской из элементов <canvas>. Каждый раз, когда два пузырька сталкиваются, часть скрывающей маски удаляется. Чтобы создать эффект рассеянной прозрачности, к компоненту непрозрачности маски применяется фильтр Гаусса. Этот процесс также включает многочисленные вычисления с плавающей запятой, выполняемые в элементах типизированных массивов, если это поддерживается браузером.
Над парящими пузырьками находится сенсорная поверхность, реагирующая на сенсорный ввод при наличии поддержки в используемом браузере, а при отсутствии таковой — на события мыши. В ответ на касание моделируется магнитное отталкивание, которое разбрасывает пузырьки в разные стороны.
Эффективная анимация
В Internet Explorer 10 мы внедрили поддержку для API requestAnimationFrame. Мы по-прежнему рекомендуем применять этот API (вместо setTimeout или setInterval) на браузерах с соответствующей поддержкой при создании приложений JavaScript с анимацией. Как было сказано в статье об оптимизации работы аппаратного обеспечения, этот API позволяет достичь максимальной частоты смены кадров на имеющемся дисплее без излишней нагрузки, которая останется невидимой для пользователя. Минимизируя число операций для оптимизации своей работы, вы можете продлить время работы батареи. Internet Explorer 10 Release Preview поддерживает этот API без префикса поставщика, но совместимость с предыдущими версиями Internet Explorer 10 Release Preview поддерживается и для версии с префиксом. Демонстрационная версия приложения "Пузырьки" поддерживает этот API, а при его отсутствии переключается на setTimeout.
Demo.prototype.requestAnimationFrame = function () {
var that = this;
if (window.requestAnimationFrame)
this.animationFrameTimer =
window.requestAnimationFrame(function () { that.onAnimationFrame(); });
else
this.animationFrameTimer =
setTimeout(function () { that.onAnimationFrame(); }, this.animationFrameDuration);
}
Demo.prototype.cancelAnimationFrame = function () {
if (window.cancelRequestAnimationFrame)
window.cancelRequestAnimationFrame(this.animationFrameTimer);
else
clearTimeout(this.animationFrameTimer);
}
Преобразование DOM-значений из строковых в числовые
JavaScript отличается высокой гибкостью и содержит набор автоматических преобразований между значениями различных типов. Например, при выполнении арифметических операций строковые значения автоматически преобразуются в числовые. Однако в современных браузерах эта удобная функция может сильно сказаться на производительности. Код для конкретного типа, составляемый лучшими компиляторами JavaScript, очень эффективен для арифметических операций со значениями известных типов, но в случае значений неожиданных типов этот же код повлечет серьезный рост нагрузки.
Когда демонстрационная версия приложения "Пузырьки" загружена, свойство numberOfBubbles
начинает со значения 100. В каждом кадре анимации расположение каждого пузырька корректируется:
function Demo() {
this.numberOfBubbles = 100;
//...
}
Demo.prototype.moveBubbles = function(elapsedTime) {
for (var i = 0; i < this.numberOfBubbles; i++) {
this.bubbles[i].move(elapsedTime, this.gravity);
}
}
Если пользователь выбирает иное значение в интерфейсе, значение свойства numberOfBubbles
должно быть скорректировано соответствующим образом. Простой обработчик событий может выполнить это следующим образом:
Demo.prototype.onNumberOfBubblesChange = function () {
this.numberOfBubbles = document.getElementById("numberOfBubblesSelector").value;
//...
}
Этот с виду естественный способ чтения введенных пользователем данных приводит к 10% росту нагрузки в части JavaScript. Поскольку значение, полученное из раскрывающегося списка и назначенное для свойства numberOfBubbles
, является строковым (а не числовым), его нужно преобразовывать при каждой итерации цикла moveBubbles
для каждого кадра анимации.
Это доказывает, что целесообразно явным образом преобразовывать значения, извлеченные из модели DOM, в числовые значения, прежде чем применять их в арифметических операциях. В JavaScript свойства модели DOM обычно заданы значениями типизированной строки, а часто выполняемые автоматические преобразования строковых значений в числовые могут оказаться затратными. Более эффективный способ обновить выбранное пользователем свойство numberOfBubbles
применительно к демонстрационной версии выглядит так:
Demo.prototype.onNumberOfBubblesChange = function () {
this.numberOfBubbles = parseInt(document.getElementById("numberOfBubblesSelector").value);
//...
}
Работа со свойствами доступа ES5
Свойства доступа ECMAScript 5 представляют собой удобный механизм для инкапсуляции данных, вычисления свойств, проверки данных и уведомлений об изменениях. В демонстрационной версии приложения "Пузырьки" при надувании каждого пузырька его радиус корректируется, поэтому вычисляемое свойство radiusChanged
указывает, что размер пузырька необходимо изменить.
Object.defineProperties(Bubble.prototype, {
//...
radius: {
get: function () {
return this.mRadius;
},
set: function (value) {
if (this.mRadius != value) {
this.mRadius = value;
this.mRadiusChanged = true;
}
}
},
//...
});
Во всех браузерах свойства доступа вызывают рост нагрузки по сравнению со свойствами данных. Точный объем дополнительной нагрузки меняется в зависимости от браузера.
Минимизация доступа Canvas ImageData
Широко принято минимизировать число обращений к модели DOM в циклах на критическом пути производительности. Например, если бы в демонстрационной версии приложения "Пузырьки" расположение каждого пузырька обновлялось с оглядкой на соответствующий элемент в документе (как показано ниже), производительность сильно пострадала бы.
Bubble.prototype.render = function () {
document.getElementById("bubble" + this.id).style.left = Math.round(this.x) + "px";
document.getElementById("bubble" + this.id).style.top = Math.round(this.y) + "px";
this.updateScale();
}
Вместо этого элемент, соответствующий каждому пузырьку, кэшируется в пузырьковом объекте в JavaScript один раз, а затем к нему выполняется прямой доступ в каждом кадре анимации.
Bubble.prototype.render = function () {
this.element.style.left = Math.round(this.x) + "px";
this.element.style.top = Math.round(this.y) + "px";
this.updateScale();
}
Важно учесть, что во избежание похожей излишней нагрузки необходимо проявлять особое внимание при работе с <canvas>. Объект, получаемый через вызов свойства canvas.getContext("2D").getImageData()
, также является объектом модели DOM. Нижеприведенный код можно использовать в демонстрационной версии, чтобы воспроизвести эффект столкновения пузырьков на canvas. В этой версии свойство imgData.data
читается при каждой итерации в цикле, что требует обращения к модели DOM и сильно увеличивает нагрузку.
BubbleTank.prototype.renderCollisionEffectToCanvas = function(px, py) {
var imgData = this.canvasContext.getImageData(/*...*/)
//...
for (var my = myMin; my <= myMax; my++) {
for (var mx = mxMin; mx <= mxMax; mx++) {
var i = (mx + gaussianMaskRadius) + (my + gaussianMaskRadius) * gaussianMaskSize;
imgData.data[4 * i + 3] = 255 * occlusionMask[(px + mx) + (py + my) * canvasWidth];
}
}
this.canvasContext.putImageData(imgData, px - gaussianMaskRadius, py - gaussianMaskRadius);
}
Более эффективный способ обновления данных изображения <canvas> состоит в кэшировании свойства data
, как показано в следующем фрагменте кода. Свойство data
является типизированным массивом (PixelArray) и легко доступно из JavaScript.
BubbleTank.prototype.renderCollisionEffectToCanvas = function(px, py) {
var imgData = this.canvasContext.getImageData(/*...*/)
var imgColorComponents = imgData.data;
//...
for (var my = myMin; my <= myMax; my++) {
for (var mx = mxMin; mx <= mxMax; mx++) {
var i = (mx + gaussianMaskRadius) + (my + gaussianMaskRadius) * gaussianMaskSize;
imgColorComponents[4 * i + 3] =
255 * occlusionMask[(px + mx) + (py + my) * canvasWidth];
}
}
this.canvasContext.putImageData(imgData, px - gaussianMaskRadius, py - gaussianMaskRadius);
}
Использование типизированных массивов для хранения чисел с плавающей запятой
В Internet Explorer 10 мы добавили поддержку для типизированных массивов. При работе с числами с плавающей запятой целесообразно использовать типизированные массивы (Float32Array или Float64Array) вместо массивов JavaScript (Array). Массивы JavaScript могут вмещать элементы любого типа, но обычно требуется, чтобы значения с плавающей запятой были расположены на куче (упаковке), прежде чем они будут добавлены в массив. Это влияет на производительность. В целях достижения стабильно высокой производительности на современных браузерах, используйте Float32Array или Float64Array, чтобы указать задачу хранения значений с плавающей запятой. Таким образом вы поможете модулю JavaScript избегать нагромождений и включать функции оптимизации компиляторов, например, создание операций для конкретных типов.
BubbleTank.prototype.generateOcclusionMask = function() {
if (typeof Float64Array != "undefined") {
this.occlusionMask = new Float64Array(this.canvasWidth * this.canvasHeight);
} else {
this.occlusionMask = new Array(this.canvasWidth * this.canvasHeight);
}
this.resetOcclusionMask();
}
На примере выше показано, как демонстрационная версия приложения "Пузырьки" использует Float64Arrays для удержания и обновления скрывающей маски, применяемой к canvas и заслоняющей фоновое изображение. Если браузер не поддерживает типизированные массивы, код переключается на обычные массивы. Эффективность от применения типизированных массивов для демонстрационной версии приложения "Пузырьки" меняется в зависимости от настроек. В окне среднего размера в Internet Explorer 10 типизированные массивы улучшают общую частоту смены кадров примерно на 10%.
Подводя итоги
В этой статье мы ознакомились с недавно выпущенной демонстрационной версией приложения "Пузырьки" и узнали, как много преимуществ можно реализовать в ней, выполняя JavaScript в Internet Explorer 10 Release Preview. Мы изучили несколько важных способов достижения высокой производительности в приложениях на базе анимации. Подробные технические сведения об изменениях в среде выполнения JavaScript (Chakra) в Internet Explorer 10 Release Preview можно найти в предыдущей статье. Нас воодушевляют достигнутые в Internet Explorer 10 Release Preview значительные улучшения в плане производительности, а также реализованные новые возможности, и мы надеемся, что все это поможет создавать еще больше интересных приложений с помощью веб-стандартов и технологий.
— Эндрю Миадович (Andrew Miadowicz), руководитель программы, JavaScript