Оптимизация скорости визуализации веб-страниц

olegchir 20 ноября в 08:38 13,8k
Нередко рассказы о том, как веб-разработчики заботятся о своих приложениях, начинаются с процесса визуализации сайта, опускаются на уровень DOM и останавливаются на утверждениях типа: «И это быстро потому, что используется ускорение». Мартин Сплитт рассказывает о производительности снизу вверх: он начинает с пикселя и поднимается уровень за уровнем, заканчивая компоновкой страницы.


В основе статьи – выступление Мартина на JavaScript-конференции HolyJS 2017 в Питере, где он рассказывает о том, как происходит визуализация в браузерах и что нужно делать для того, чтобы ваши сайты «летали».

О спикере
Мартин работает главой инженерного отдела Archilogic. Это небольшая компания, которая разрабатывает одноименный веб-сервис, позволяющий пользователям создавать виртуальные 3D-туры в браузерах на десктопах и мобильных устройствах.


Как происходит загрузка сайта из сети


Сегодня мы поговорим о том, как преобразовать текст во что-то визуальное. Мы начинаем с разметки и завершаем пикселями. Как это работает?



Первое, что необходимо знать в отношении оптимизации производительности, – это то, как именно происходит загрузка сайта.

  1. HTML извлекается из сети
  2. HTML-текст разбирается на токены по мере поступления
  3. Токены разбираются на объекты (DOM / CSSOM)
  4. Объекты располагаются на странице
  5. Объекты отрисовываются и компонуются в единое целое
  6. JS или CSS могут изменять контент

Итак, на первом этапе идет запрос на сервер, откуда необходимо получить текстовые данные (HTML, CSS, JavaScript). Получив текст, мы должны разметить его, то есть разбить на элементы и теги, чтобы работать с ним дальше.

Когда вы имеете дело с JavaScript, прежде чем начать работать с кодом, необходимо получить его целиком. HTML имеет древовидную структуру, поэтому вы можете начинать анализ, полностью получив один из элементов. Поскольку анализ выполняется по мере получения HTML-данных, он называется потоковым.

Когда у нас достаточно токенов, мы начинаем анализировать их и создавать из них объекты. Объекты имеют древовидную структуру (к примеру, в документе есть заголовок, под ним текст, изображение и т.д.). Это делается не только для HTML, но и для CSS, поэтому у нас получается два дерева – DOM и CSSOM.

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

Когда макет страницы создан, необходимо сделать так, чтобы объекты стали видимыми. А отрисованные объекты компонуются, чтобы получилась страница. Как только этот процесс завершен, можно использовать JavaScript и CSS, чтобы сделать страницы динамическими.

Теперь остановимся на этих этапах подробнее.

Как выполняется анализ страницы сайта


Анализ начинается с получения завершенных элементов — наподобие этого:

<h1>Hello HolyJS!</h1>

Это – завершенный элемент, поэтому браузер раскладывает его на элемент заголовка и текстовый узел внутри него.

<p>Lorem ipsum…</p>

Затем браузер может увидеть элемент абзаца с текстовым узлом внутри.

<p><img…></p>

И еще один абзац с изображением и т.д.

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



Вот как это выглядит для таблиц стилей. Тут мы видим внешнюю таблицу и встроенные стили:

<link rel="stylesheet" href="style.css">
<style>
    body { color: red; }
    h1    { color: blue; }
</style>
<p style="color: green">... </p>

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



Затем мы переходим к следующему элементу и смотрим, что заголовок должен быть синим, а абзац – зеленым. Таким образом мы строим объектную модель таблицы стилей.



Но мы не визуализируем ее. Когда вы открываете сайт, он выглядит не так. Вот что стоит иметь в виду для улучшения скорости визуализации на этапе анализа сайта:

  • Используйте inline-стили для важных правил CSS (цвет, расположение на странице и пр.), чтобы внешние CSS не тормозили построение дерева.
  • При анализе обращайте внимание на порядок, в котором вы получаете исходные HTML-данные.
  • Сведите к минимуму повторения при построении дерева.
  • Используйте визуализацию на стороне сервера.

Еще момент. При анализе анимации и таблиц на предмет того, насколько быстро они визуализируются, вряд ли можно отловить все проблемы в автоматическом режиме, всегда приходится тестировать вручную. Я знаю, что разработчики Chrome и Firefox уже работают над улучшением инструментов измерения производительности. Среди них инспекторы анимации, которые сообщают, насколько часто она отрисовывается, новые JavaScript API, измерение времени компоновки. Кстати, уже сейчас можно оценивать время отрисовки. Мы сами используем эту возможность, оценивая, насколько быстро визуализируется сцена. Если это происходит слишком медленно, мы отключаем некоторые вещи и тестируем снова.

Расположение объектов на странице


Теперь рассмотрим, как происходит верстка. К примеру, у нас есть класс с каким-то текстом.

<div class="box">
Hello world
</div>

Браузер располагает прямоугольник с текстом на всю ширину страницы



Дальше есть какое-то изображение

<div class="box">
Hello world
</div>
<b><img src="yay.png"></b>

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



Теперь к этому добавляется CSS:

<style>
.box {
   display: inline-block;
   width: 50%;
}
</style>

И оказывается, что прямоугольник – это inline-block, который должен занимать только половину ширины страницы. А раз так, то для изображения появляется место выше, поэтому мы переносим его туда.



Примерно так работает верстка в браузере.

Вот о каких вещах, влияющих на скорость визуализации, стоит помнить на этапе верстки сайта:

  • Важно уяснить, как будет выглядеть страница на уровне макета, а не содержания (где расположены блоки и какого они размера).
  • Верстка основана на CSSOM + DOM, то есть если у вас произошли важные изменения в JavaScript, необходимо переделывать верстку.
  • Верстка определяет реальный размер каждого элемента (при изменении размеров элементов нужно делать верстку повторно).
  • Верстка может влиять на производительность, поэтому старайтесь избегать изменений в верстке.
  • Изменения в верстке могут повлечь за собой повторную отрисовку элементов, что также влияет на производительность (например, при изменении размеров изображения необходимо отрисовывать его по новой).
  • При анимации верстка может «слететь».

Отрисовка объектов


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



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

Вот что нужно иметь в виду в отношении отрисовки:

  • Она не происходит последовательно.
  • Это может влиять на производительность (помните, что разрешение Full HD составляет 1920×1080, это около двух миллионов точек).
  • При этом задействуется память (даже для изображения с разрешением 500х500 пикселей, каждый из которых имеет объем 3 байта, необходимо 750 килобайт памяти).

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



Мы не перерисовываем все эти пиксели, мы просто немного изменяем цифры, получая, например, анимацию летающего по экрану кота.



Скомпоновать эти изображения можно при помощи блендинга – изменения режимов наложения. В процессе блендинга к двум исходным слоям применяется шейдер – функция, которая показывает, каким образом должны смешиваться слои. Вот как это может выглядеть в псевдокоде:

const shader = (x, y, layers, blend, filter) => {
    return filter(
       blend(x, y, layers) // returns colour
   ) // returns final colour
}

Иными словами, для определенного пикселя берутся значения всех слоев и объединяются определенным образом. Существует 15 режимов наложения (multiply, screen и другие), каждый из которых по сути представляет собой формулу, по которой объединяются цвета. Например, при использовании режима Multiply значения умножаются:

color(x,y) = cat(x,y) * sky(x,y)

При этом мы получим вот такого кота:



После получения смешанного цвета мы можем запустить еще одну функцию – фильтр. Примерами фильтров являются непрозрачность, контрастность, инвертирование цветов, сепия, насыщенность, отбрасывание тени, преобразование в оттенки серого и пр. К примеру, при использовании фильтра Grayscale числовые значения для полученного цвета складываются, а затем делятся на три:

(r, g, b) => (r + g + b) / 3

Таким образом, если цвета пикселя были достаточно яркими, при преобразовании в оттенки серого мы также получим светлое изображение, а если они были блеклыми – темное.

А вот как эта функция Grayscale выглядит на самом деле:

varying highp vec2 coord;
uniform sampler2D layer;
 
void main(void) {
   vec4 color = texture2D(layer, vec2(coord.s, coord.t));
   float grayScale = (color.r + color.g + color.b) / 3.0;
  gl_FragColor = vec4(grayScale, grayScale, grayScale, 1.0);
}

Важно, что при использовании режимов смешивания и фильтров разные браузеры ведут себя не совсем одинаково. Chrome – это единственный браузер, который переработал архитектуру так, что визуализация происходит только с использованием слоев. Остальные браузеры работают примерно одинаково с CSS-фильтрами, однако SVG-фильтры по непонятной для меня причине повторно отрисовываются. Это происходит в большинстве браузеров: Firefox, Safari, Internet Explorer, Edge.

Итак, что дает компоновка:

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

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

  • video, canvas – для видео и игр, которые содержат большое число изменяющихся кадров;
  • 3D-трансформации;
  • композитные анимации (те, где задействованы перемещение, вращение, масштабирование данных);
  • новое свойство will-change, при помощи которого можно указать, что именно вы планируете изменять много раз.

Свойство will-change – это подсказка для браузера, а чем больше подсказок, тем лучше будет происходить визуализация. То есть если вы говорите браузеру: «У меня есть этот Canvas, и я собираюсь изменить его расположение или размер», то, скорее всего, браузер сможет обработать страницу быстрее.

Давайте поиграем: практические примеры


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

#transform {
   transform: translateX(150px);
}
setTimeout(() => {
   el.style.transform = 'translateX(0)'
}, 2000)

Да, будет, ведь это 2D-трансформация. Никакие слои тут не используются, и пиксели будут отрисованы повторно.

Теперь второй раунд игры, с другим кодом. Он делает то же самое, но с другим перемещением. А будет ли отрисована эта анимация?

#transform { transform: translate3d(150px, 0,0); }
 
setTimeout(() => {
    el.style.transform = 'translate3d(0, 0, 0)'
}, 2000)

Нет, потому что тут используются 3D-трансформации, а значит, слои.

Наконец, третий раунд. Что это?

@keyframes move {
     0% { left: 0; }
    100% { left: 200px; } }

Это то, чего не стоит делать ни в коем случае. Но я могу изменить правила игры, используя тут свойство will-change. Сейчас у нас есть двухсекундная анимация, и она может сместиться влево. Как вы думаете, можно ли использовать такой код?

#transform {
  will-change: left;
  animation: move 2s infinite; }

Можно, но только в том случае, если элемент не зависит от других в верстке (например, положение элемента является абсолютным). Если же его положение относительно, то при изменении его положения другие элементы на странице будут смещаться.

Итак, вот о чем нужно помнить касательно слоев и компоновки:

  • Стоит избегать повторной верстки и отрисовки – это снижает производительность.  Слои могут быть полезны, но будьте осторожны. Не создавайте их, если вы не уверены наверняка, что они необходимы, чтобы не задействовать слишком много памяти.
  • Сначала измеряйте, затем оптимизируйте. Тестируйте ваши анимации, используя консоль разработчика в браузере, наблюдайте за тем, насколько часто происходит отрисовка элементов, замеряйте использование памяти.
  • Старайтесь использовать компоновку везде, где это возможно (задействуйте свойство will-change, 3D-трансформации и т.д.)

Тут как раз будет уместна цитата Пола Льюиса: «Производительность – это искусство избегать работы». Всегда помните, что, когда речь заходит о производительности, самое лучшее, что вы можете сделать – это делать меньше.

Сравнение производительности Canvas 2D и WebGL


Давайте еще немного поговорим о том, когда лучше использовать Canvas 2D, а когда — WebGL.

Вот простой пример использования Canvas 2D. Предположим, размер нашего холста равен размеру HD-кадра. Мы располагаем множество объектов в разных местах, делаем их произвольного размера и затем рисуем изображение несколько раз в разных местах и с разными размерами. Это не так уж сложно:

for(var i=0; i<NUM_OBJECTS; i++) {
   var x = Math.random() * HD_WIDTH,
       y = Math.random() * HD_HEIGHT,
      size = Math.random() * 512
  ctx.drawImage(img,
  x - HALF_SIZE, y - HALF_SIZE, size, size
  )
}

На оси Y мы можем видеть число кадров в секунду, а на оси Х – количество объектов, которые мы собираемся сделать. То есть один и тот же объект отрисовывается один раз, сто, двести пятьдесят, пятьсот и тысячу раз.



Все, что выше зеленой линии, означает суперплавную отрисовку с частотой кадров более 60 в секунду. Оранжевая линия показывает, при каком количестве кадров отрисовка еще происходит плавно, хоть и не так быстро, как хотелось бы. Красная – это черта, за которой отрисовка выполняется рывками, в режиме эдакого слайд-шоу (вместо плавной анимации). Как мы можем видеть, если число объектов равно 250, то число кадров в секунду значительно ниже допустимого.

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



Тут меня могут спросить: «А что если использовать аппаратное ускорение?» Как видно по результату теста ниже, при визуализации 10000 объектов WebGL дает как минимум 20 кадров в секунду. Это не очень круто, но в принципе допустимо. Но с Canvas 2D мы получаем всего 7 кадров в секунду. А при 50000 объектах визуализация будет происходить очень медленно в обоих случаях, но WebGL даст слайд-шоу, скорость которого все равно в два раза выше.


Именно поэтому стоит использовать WebGL не только для 3D, но и для 2D-содержимого тоже.

Однако не стоит думать, что нужно делать ВСЁ с использованием WebGL, потому что это быстро. Я всегда говорю, что это плохая идея. Нужно использовать правильные инструменты, а не изобретать технологии с нуля, как это происходит, если вы начинаете делать абсолютно все средствами WebGL.

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

HTML+CSS используются для создания:

  • Семантического и доступного контента
  • Графических примитивов

SVG применяется для:

  • Изображений, которые хорошо масштабируются

Canvas 2D удобен для создания:

  • Простой 2D-анимации

WebGL стоит применять для:

  • 3D-контента и любых игр

Я думаю, что когда-нибудь веб-анимации смогут проигрываться так же быстро, как раньше flash-анимации. Движки визуализации в браузерах постоянно улучшаются. Если вы возьмете движок браузеров 2010 года и сравните его с современным, вы увидите большую разницу. Да, конечно, пока еще движки несовершенны. Главная проблема современных браузеров в том, что они были созданы для менее динамических страниц, теперь они постоянно должны приспосабливаться к интерактивным графическим приложениям.

Возможно, один из лучших примеров того, что мы увидим в ближайшем будущем – это экспериментальный движок Servo от Mozilla. Он использует аппаратное ускорение везде, где это возможно, поэтому там, где в других браузерах визуализация происходит с частотой 60 кадров в секунду, в Servo вы получаете 120 FPS.



Тем, кому близка тема производительности приложений, наверняка понравится новый доклад Мартина Better, faster, stronger — getting more from the web platform, с которым он выступит на HolyJS в Москве.

Вам могут быть также интересны и другие доклады, например:


Вся программа конференции доступна на сайте, билеты можно приобрести тут.
Проголосовать:
+22
Сохранить: