Основы производительности
Английское слово Performance, которое используется в статьях о производительности приложений, также можно перевести, как "эффективность". Этот документ объясняет основы производительности, того как браузеры помогают улучшить её и какие инструменты и процессы вы можете использовать, чтобы её улучшить.
Что такое производительность?
Ощущаемая пользователем "производительность" - это единственная производительность, которая имеет значение. Пользователи взаимодействуют с системой с помощью ввода каких-то данных: прикосновений, движения и речи. В ответ, они получают реакцию, основанную на зрительном, тактильном или слуховом аппаратах. Производительность - это качество того, как система реагирует на действия пользователя.
При прочих равных, код, оптимизированный для каких-то иных целей, кроме ощущаемой пользователем производительности (здесь и дальше UPP, user-perceived performance) всегда проигрывает коду, который оптимизирован для UPP. Упрощённо говоря, пользователи предпочитают отзывчивое и плавное приложение, которое обрабатывает 1,000 транзакций к базе данных в секунду грубому неотзывчивому приложению, которое обрабатывает 100,000,000 запросов в секунду. Конечно, это не означает, что другие метрики становятся ненужным, но первой вашей целью должна быть UPP.
Следующие разделы укажут и объяснят некоторые метрики производительности:
Отзывчивость
Отзывчивость - это то, как быстро система предлагает ответ (или множество ответов) на запрос пользователя. Например, когда пользователь нажимает на экран, он ожидает, что пиксели под пальцем изменятся каким-то образом. Для этого случая взаимодействия хорошей метрикой будет время, которое прошло между моментом нажатия и изменением пикселей.
Отзывчивость иногда включает в себя несколько этапов. Запуск приложения - один из важнейших этапов. Мы обсудим его ниже.
Отзывчивость важна просто потому, что пользователи теряются и злятся, когда их игнорируют. Ваше приложение игнорирует пользователя каждую секунду, когда оно не отвечает на пользовательский ввод.
Частота кадров
Частота кадров - это частота, с которой система перерисовывает пиксели, отображаемые пользователю. Это знакомая концепция: каждый предпочитает, скажем, игры, которые работают в режиме 60 кадров в секунду играм, которые работают с частотой 10 кадров в секунду. Даже если они не смогут объяснить причины этого.
Частота кадров важна примерно так же, как "качество обслуживания". Дисплеи устройств спроектированы так, чтобы обманывать глаза пользователей, доставляя фотоны света так, чтобы изображение было похожим на реальное. Например, бумага, покрытая напечатанными буквами, отражает фотоны определённым образом. Манипулируя рендерингом, приложения-читалки пытаются отправить фотоны похожим образом, обманывая глаза.
Ваш мозг знает, что движение - это не отрывчатый или дискретный процесс, а плавный и последовательный. Дисплей устройств с высокой частотой кадров сделаны просто для того, чтобы сделать эту иллюзию более реальной. (Интересно, что стробоскопы переворачивают эту концепцию, заставляя наш мозг создавать иллюзию дискретной реальности).
Примечание: Люди обычно не могут почувствовать разницу между частотами кадров выше 60Hz. По этой причин большая часть современных электронных дисплеев спроектированы для обновления картинки с такой частотой. Однако, для некоторых живых существ такая частота кадров будет казаться замедленной. Например, для колибри.
Использование памяти
Использование памяти - это отдельная ключевая метрика. В отличии от отзывчивости и частоты кадров, пользователи не могут напрямую почувствовать использование памяти, но её использование влияет на "состояние пользователя". Идеальная система будет поддерживать 100% состояния всех приложений всё время: все приложения будут запускаться одновременно, а каждое приложение будет возвращаться к состоянию, которое было в последний раз, когда пользователь с ним взаимодействовал (состояние приложения хранится в компьютерной памяти - поэтому это сравнение его с user state довольно точное).
Отсюда следует одно неочевидное заключение: хорошо спроектированная система не заботится об увеличении свободной памяти. Память - это ресурс, а свободная память - неиспользуемый ресурс. Наоборот, хорошо спроектированная система использует достаточно много памяти, чтобы обеспечить такое состояние, когда пользователь не чувствует изменений в производительности.
Это не означает, что система должна тратить память. Когда система использует больше памяти, чем это необходимо для поддержания состояния приложения - она тратит память. В этом случае, другое приложение (или даже другое состояние), которые могли бы использовать эту память - не могут её использовать. На практике, ни одна система не может поддерживать в памяти все состояния одновременно. Разумное распределение памяти, достаточное для поддержки состояния приложения - это важная проблема, которую мы рассмотрим позже.
Использование энергии
Последний показатель, который нужно упомянуть - это потребление энергии. Подобно использованию памяти, пользователь чувствует потребление энергии опосредованно, отмечая время, через которое устройство начинает изменять воспринимаемую пользователем производительность (UPP). Для минимизации отрицательных эффектов использования энергии, мы должны делать систему экономной.
Для примера, вспомните, как работают мобильные устройства: вы можете включить режим энергосбережения, когда отключаются другие системы. Но есть и более жёсткий режим, который включается автоматически, когда заряд уменьшается до 5% - система включает троттлинг процессора, замедляя выполнение всех инструкций.
Всю оставшуюся часть статьи мы будем обсуждать производительность в свете этих показателей.
Оптимизация платформы
В этой секции приводится краткий обзор того, как Firefox/Gecko вкладывается в производительность в целом, не заостряя внимание на конкретных приложениях. Для разработчиков и пользователей это будет ответ на вопрос "Как платформа помогает мне?".
Web технологии
Web платформа предоставляет много инструментов, некоторые из которых лучше подходят для конкретных задач, а некоторые - хуже. Логика приложений пишется на JavaScript. Для отображения графики разработчики могут использовать HTML или CSS (т.е. используются высокоуровневые декларативные языки); разработчики также могут использовать низкоуровневые интерфейсы, доступные в <canvas>
(включая WebGL). Где-то между HTML/CSS и Canvas лежит SVG, который предлагает некоторые преимущества обеих систем.
HTML и CSS значительно увеличивают производительность, иногда снижая частоту кадров и не давая контролировать каждый пиксель при рендере. Текст и изображения перерисовываются автоматически, UI-элементы автоматически получают системную тему, а система предоставляет некоторую "встроенную" поддержку для некоторых случаев, о которых разработчик может и не задумываться изначально. Например, отображение контента при разных разрешениях.
Элемент Холст (canvas
) предоставляет прямой доступ к пиксельному буферу, где разработчик может рисовать.Это даёт разработчику возможность контролировать каждый пиксель во время рендеринга, точно контролировать частоту кадров; но тогда разработчик должен иметь в виду работу с большим количеством разрешений экранов и ориентаций; RTL языками и т.д. Разработчики, работающие напрямую с холстами, используют либо знакомое 2D API, либо API WebGL, достаточно "близкий к железу" и по большей части придерживающийся OpenGL ES 2.0.
Как рендерит Gecko
Движок Gecko JavaScript поддерживает just-in-time (JIT) компиляцию. Благодаря этому, логика приложения выполняется примерно так же, как это происходит в других приложениях на виртуальных машинах - например, Java Virtual Machines - а в некоторых случаях эффективность этих приложений близка к "нативному коду".
Инструменты, необходимые для работы с HTML, CSS и Canvas оптимизированы несколькими путями. Например, композиция (этап, известный как Layout) в HTML/CSS и код, отвечающий за графику в Gecko, сделаны таким образом, чтобы уменьшить количество операций ревалидации и перерисовки для общих случаев (например, скроллинг); эти оптимизации включены "по умолчанию", поэтому разработчики пользуются ими бесплатно. Буферы пикселей, отрисованных как для Gecko "автоматически", так и для canvas
"вручную", минимизируют количество копий, которые передаются в буфер кадров дисплея. Чтобы достичь этого, Gecko старается избегать создания промежуточных слоёв (например, во многих других операционных системах создаются пиксельные фоновые буферы для каждого отдельного приложения), но взамен Gecko использует специальные участки памяти для хранения графических буферов. К этой памяти имеет прямой доступ железо, которое ответственно за формирование картинки. Сложные сцены рендерятся с использованием графического адаптера (видеокарты). А простые сцены, для экономии энергии, рендерятся специальным выделенным железом для композиции, в то время, как графический адаптер находится в режиме ожидания или выключен.
Для продвинутых приложений полностью статичный контент скорее является исключением, чем правилом. Такие приложения используют динамический контент, анимируемый с помощью animation
и transition
. Переходы и анимации особенно важны для приложений: разработчики могут использовать CSS для объявления сложного поведения с помощью простого высокоуровнего синтаксиса. С другой стороны, движок Gecko хорошо оптимизирован для того, чтобы рендерить такую анимацию эффективно. В целом, общепринятые анимации передаются к обработке системному компоновщику, который может отрендерить их в эффективном, энергосберегающем режиме.
Производительность приложений
Эта секция попытается ответить на вопрос "как сделать приложение быстрее?".
Скорость загрузки
Загрузка приложения может быть поделена на три этапа, которые влияют на UPP:
- Первая отрисовка. Момент, когда приложение загрузило достаточно данных и ресурсов, чтобы отрисовать первый - начальный - кадр
- Начало интерактивности - например, когда пользователю становится доступна возможность нажать кнопку, а приложение может ему ответить
- Полная загрузка - например, когда все пользовательские альбомы перечислены в музыкальном плеере
Секрет быстрой загрузки требует двух вещей: UPP (ощущаемая пользователем скорость) - это единственное, что имеет значение; эта скорость зависит от критического пути рендеринга (Critical Rendering Path). Критический путь - это единственный и необходимый код, который должен запускать перечисленные выше события.
Например, отрисовка первого кадра, который содержит в себе необходимый HTML и CSS включает:
- Браузер должен спарсить HTML
- DOM должен быть построен для этого HTML
- Ресурсы (изображения, видео и др.) для этой модели DOM должны быть загружены и декодированы
- CSS стили должны быть применены к DOM и должен быть сформирован CSSOM
- Стилизованный компонент должен быть подготовлен к рендеру
В этом списке вы не увидите "загрузить JS файл, который нужен для второстепенного меню", "забрать и декодировать изображения для списка достижений" и т.п. Эта работа не должна выполняться при запуске приложения.
Звучит очевидно, но для достижения лучшей воспринимаемой скорости загрузки нужно запускать только тот код, который необходим и критичен для запуска приложения. Короткий путь увеличивает скорость.
Web-платформа очень динамична. JavaScript - это динамически типизированный язык, а Web разрешает загружать код, HTML, CSS, изображения и другие ресурсы динамически. Эти функции могут быть использованы для того, чтобы отложить загрузку ресурсов; чтобы сократить критический путь, подвинув загрузку лишнего контента на несколько моментов позже. Такой подход называется "ленивой загрузкой".
Другая проблема, которая может привести к ненужному простою - это ожидание ответа на запросы (например, запрос к базе данных). Чтобы избегать этой проблемы, приложение должно запрашивать данные как можно раньше, ещё во время запуска программы. Тогда к моменту, когда данные понадобятся - они уже будут в системе и приложению не придётся ждать.
Примечание: Для дополнительной информации об ускорении запуска ознакомьтесь с Оптимизацией производительности запуска.
Следует также отметить, что ресурсы, закешированные локально, могут быть загружены гораздо быстрее, чем динамические данные, загруженные через мобильную сеть с её задержками или узким каналом. Локальное кеширование и работа в офлайне могут быть достигнуты с помощью Service Workers. См. Making PWAs work offline with Service workers для подробностей.
Частота кадров
Первая важная вещь для высокой частоты кадров - это выбор правильного инструмента. Используйте HTML и CSS для создания контента, который будет в основном статическим, прокручиваемым и редко анимируемым. Используйте Canvas для создания высокодинамичного контента, например, игр, которые требуют серьёзного контроля рендеринга и не нуждаются в темах.
При отрисовывании контента в Canvas, разработчик должен сам позаботиться о достижении целей по частоте кадров, ведь он получает полный контроль над всем, что отрисовывается.
При использовании HTML и CSS разработчику необходимо использовать правильные примитивы. Firefox очень хорошо оптимизирован для скролла любого контента. Обычно это не является проблемой. Но очень часто, разменивая качество и стабильность на скорость, мы идём на ухищрения, которые могут "переоптимизировать" страницу так, что частота кадров будет выше нужной нам. Так как глаз всё равно слабо различает FPS больше 60, нет необходимости в таких оптимизациях. Одна из таких оптимизаций - использование статического рендера вместо CSS-градиента. В некоторых случаях это излишне. Чтобы не применять оптимизацию, вы можете воспользоваться медиавыражениями, которые позволят использовать подобные решения только для конкретных устройств.
Множество приложений используют Transitions и Animations для перехода между страницами или панелями. Например, когда пользователь нажимает кнопку "Настройки", чтобы перейти на другой экран; или для вызова поп-апа. Firefox оптимизирован для выполнения переходов и анимаций для сцен, которые:
- Используют страницы/панели равные по размеру дисплею или меньше
- Используют для переходов/анимаций свойства
Transform
иOpacity
Переходы и анимации, которые придерживаются этих правил, будут выгружены в системный компоновщик и выполнены максимально эффективно.
Использование памяти и энергии
Проблема улучшения использования памяти и энергии так же важна для ускорения запуска: не делайте ненужную работу и не загружайте ненужные ресурсы. Используйте эффективные структуры данных и уделяйте внимание оптимизации ресурсов.
Современные ЦПУ могут работать в режиме энергосбережения, когда они не задействованы. Приложения, которые постоянно запускают таймеры или продолжают ненужные анимации, мешают процессору перейти в режим энергосбережения. Эффективные приложения не должны так делать.
Советы к применению в коде
Следующие советы помогут вам улучшить один или несколько факторов производительности, которые мы обсуждали ранее.
Используйте CSS animation и transition
Вместо использования функции animate()
какой-нибудь библиотеки, которая, вероятно, использует много плохих решений (например (window.setTimeout()
или анимирование top / left), используйте CSS-анимации. Во многих случаях, вы можете использовать CSS Transitions. Использование этих свойств поможет, так как браузер спроектирован так, чтобы оптимизировать эти эффекты и переносить часть вычислений на GPU, чтобы они работали плавно и с минимальным влиянием на процессор. Другое преимущество - вы можете определить эти анимации в CSS декларативным образом, а не в бизнес-логике приложения.
CSS-анимации дают вам очень точный контроль эффектов, если вы используете keyframes. Более того, вы сможете отслеживать события, которые происходят во время анимации, так как основной поток обработки не блокируется. Вы можете с лёгкостью запускать анимации с помощью :hover
, :focus
или :target
. Или динамически добавляя или удаляя классы элемента.
Если вы хотите создавать анимации "на лету" или изменять их с помощью JavaScript, Джеймс Лонг написал простую библиотеку для этого - CSS-animations.js.
Используйте CSS трансформацию
Вместо того, чтобы изменять абсолютное позиционирование и возиться с этой математикой вручную, используйте свойство transform
, чтобы изменить позицию, масштаб и некоторые другие аспекты вашего контента. Именно так вы используете аппаратное ускорение. Браузер умеет передавать такие задачи графическому процессору, давая возможность ЦПУ заняться другими важными вещами.
К тому же, трансформация даёт возможности, которых в ином случае у вас не было бы. Вы не только можете манипулировать элементом в двумерном пространстве, но можете трансформировать его в 3D, изменять его наклон (скашивать, skew), поворачивать и др. Пол Айриш опубликовал статью in-depth analysis of the benefits of translate()
, в которой проанализировал работу translate с точки зрения производительности. Используя translate/transform вы используете правильный декларативный инструмент и возлагаете ответственность за его оптимизацию на браузер. Вы так же получаете возможность с лёгкостью позиционировать элементы. Если вы будете использовать только top
и left
, вам придётся написать некоторый дополнительный код, чтобы предусмотреть некоторые особенности такого позиционирования. И последний бонус - с Transform / Translate вы будете работать примерно так же, как работали бы с элементом canvas
.
Примечание:
В некоторых случаях (в зависимости от платформы) вам может понадобиться добавить свойство translateZ(0.1)
, если вы хотите заставить клиента перенести вычисление анимаций на графический адаптер. Как было упомянуто выше, это может улучшить производительность, но увеличит потребление памяти. Какое из зол - меньшее - решать вам. Протестируйте оба варианта и выясните, что лучше подходит для вашего приложения.
Используйте requestAnimationFrame()
вместо setInterval()
Вызовы window.setInterval()
запускают код с учётом предполагаемой частоты кадров. Однако, эта ожидаемая частота и фактическая не всегда совпадают. SetInternal говорит браузеру "нарисуй результаты", даже если браузер не занимается сейчас отрисовкой - такое случается, когда графический адаптер ещё не дошёл до следующего цикла обновления экрана. Такое несовпадение таймингов - чрезмерная растрата процессорного времени и даже может приводить к снижению срока службы аккумуляторов пользовательского устройств.
Вместо этого, вам необходимо использовать window.requestAnimationFrame()
. Эта функция ждёт, пока клиент не будет готов к формированию следующего кадра анимации и не будет досаждать своими запросами аппаратную часть устройства, если устройство не занимается в данный момент рендерингом. Другое преимущество этого API в том, что такие анимации не будут запускаться, пока ваше приложение не видно на экране (например, если оно выполняется в фоне или занимается другими операциями). Это сохранит батарею и защитит вас от проклятий, которые пользователи будут выкрикивать в небо.
Делайте события мгновенными
Как старомодные, заботящиеся о доступности веб-разработчики, мы любим событие нажатия, так как оно также срабатывает и для ввода с клавиатуры. На мобильных устройствах это работает иначе. Вы должны использовать touchstart
и touchend
, а не click. Причина этому - в мобильных устройствах срабатывает задержка в несколько сотен миллисекунд между касанием экрана и запуском обработчика click. Из-за этого приложение может ощущаться медленным. Если вы будете тестировать ваше приложение на предмет работы с касаниями, вы не пожертвуете доступностью. Кроме того, существуют библиотеки, которые ускорят разработку. Например, Financial Times использует библиотеку fastclick для обработки.
Держите интерфейс простым
Одна из самых больших проблем, которую мы нашли в HTML5 приложениях, заключается в том, что перенос большого количества DOM элементов делает приложение медленным - особенно, если в этих элементах много градиентов и теней. Упрощайте то, как выглядит ваше приложение и передвигайте проксирующий элемент, когда выполняете Drag And Drop - это сильно ускорит работу.
Когда, например, у вас есть большой список элементов (скажем, твитов), не перемещайте их все. Вместо этого, держите в вашем DOM-дереве только те элементы, которые видимы и несколько за областью видимости, чтобы при скролле не было заметно моргание. Остальные - прячьте. Сохраняйте данные в JavaScript объектах, вместо хранения данных и доступа к ним в DOM. Думайте об экране, как об устройстве представления необходимых данных, а не всех. Это не означает, что вы не можете использовать HTML, как источник данных; просто читайте его один раз, а затем продвигайтесь на 10 элементов, изменяя содержимое первого и последнего элемента, согласно их позициям в списке, вместо того, чтобы двигать 100 элементов, которые уже невидимы. Подобный трюк сработает и в играх со спрайтами: если они не видны на экране, нет необходимости запрашивать их. Вместо этого переиспользуйте элементы, которые выходят за пределы экрана, в то время как новые входят.
Общий анализ производительности
Firefox, Chrome и другие браузеры предоставляют встроенные инструменты, которые могут помочь вам диагностировать медленный рендеринг. В частности, Firefox's Network Monitor покажет точное время, когда произошёл каждый сетевой запрос, насколько большим он был и как долго обрабатывался.
Если на вашей странице есть JavaScript, который выполняется очень долго, JavaScript profiler укажет на наиболее медленные строки кода.
Встроенный Gecko Profiler - очень полезный инструмент, который позволяет вам получить ещё более подробную информацию о том, какие части кода работают медленно. Это довольно сложный инструмент, но он предоставляет множество деталей.
Примечание: Можно использовать эти инструменты и в Android браузере, запустив Firefox и включив remote debugging.
Например, множественные сетевые запросы в мобильной сети занимают больше времени. Рендеринг больших изображений и CSS градиентов может происходить дольше. Простое скачивание больших изображений может занять большее время, даже через быструю сеть, так как аппаратная часть мобильных устройств зачастую слишком медленна, чтобы использовать все возможности быстрых каналов данных. Для получения полезных подсказок о производительности на мобильных устройствах, ознакомьтесь с докладом Максимилиано Фёртмана Mobile Web High Performance.
Тестовые кейсы и сообщения об ошибках
Если инструменты разработчика в Firefox или Chrome не помогают вам выяснить проблему или выглядит так, что браузеры сами по себе создают проблему, попробуйте сформировать тестовое окружение, которое максимально изолирует проблему. Это очень часто помогать диагностировать проблему.
Убедитесь, что вы можете воспроизвести проблему, просто сохраняя и загружая статическую копию HTML-страницы (включая изображения/стили/скрипты). Если так - удалите из HTML файла всю личную информацию и обратитесь к за помощью (отправьте отчёт в Bugzilla или, например, сохраните файл на своём сервере и поделитесь ссылкой). Вам также понадобится поделиться информацией профилировщика, которую вы собрали с помощью инструментов отладки.