image

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

Начнём издалека, с той штуки с которой вы всё это читаете. Большинство экранов обновляют изображение примерно 60 раз в секунду и это создаёт иллюзию движения. При каждом обновлении изображение незначительно меняется, поэтому нам кажется что все преобразования происходят плавно. По сути речь идёт о кадрах, как в киноплёнке.

Если изображение (в нашем случае веб-страница) не успевает отрисоваться, мы можем различить что оно дёргается, плавность пропала, и мы пропускаем кадры. Чтобы показать 60 кадров в течении 1 секунды (1000мс) нам необходимо показывать новый кадр примерно за 16,6 мс. Иначе говоря, если мы видим скачки движения — новое изображение не успевает отрисоваться за 16 миллисекунд.

Чтобы определить проблему, нам надо рассмотреть этапы того как браузер создаёт страницу и понять чем занят процессор вместо полезной и нужной деятельности. Подробно весь процесс сборки страницы описан в работе Тали Гарсиель “Как работает браузер”. Если упрощать, то при загрузке страницы браузер разбирает html и css на узлы, формирует из них деревья, объединяет их, и рассчитывает то, как должен выглядеть каждый узел.

Далее происходят два процесса:
  • Layout — рассчитывается положение и размер элементов.
  • Paint — применяются стили и непосредственно отрисовываются элементы.


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

Мы можем убедиться в этом заглянув в Chrome Dev Tools и воспользовавшись Timeline (на май 2015 он выглядит примерно так).

image

Timeline позволяет записать разные активности браузера во время взаимодействия с ним и оценить сколько они занимают процессорного времени. Если речь идёт о рендеринге, то можно оценить сколько стоит процессорного времени перерисовать тот или иной элемент. Не все они при перерисовке имеют одинаковую стоимость. Какие-то более дорогостоящие и сложные, другие менее. Определяется это эмпирическим путём, записывая и изучая таймлайн.

В Dev Tools также есть опция Show paint rectangles, которая позволяет определить происходит ли перерисовка в данный момент. Элемент, который перерисовывается, подсвечивается. Включается эта опция во вкладке Rendering.

image

Рассмотрим простой пример. Допустим у нас есть некий блок, который мы анимировали достаточно простым способом — сменой свойства left у абсолютно позиционированного элемента.

.elem{
      width: 200px;
      height: 100px;
      background-color: lightgray;
      color: white;
      position: absolute;
      left: 100px;
      top: 100px;
      transition: 1s ease;
}
.elem--active{
      left: 400px;
}


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

image

Как мы можем это сделать? Если посмотреть спецификацию (http://www.w3.org/TR/css3-transforms/#transform-property), то там в описании свойства translate написано, что если у свойства есть значение отличное от пустого, то это создаст отдельный контекст, и элемент будет выведен в новый слой.

Исторически, браузеры объединяли всю веб-станицу в один слой, однако со временем возникла потребность выделять отдельно некоторые элементы и управлять ими при помощи графического процессора (GPU). Это значительно сокращает нагрузку на центральный процессор.

Перепишем наш CSS для использования с transform: translateX().

.elem--active{
      transform: translateX(400px);
}


А теперь и попробуем вызвать тоже самое взаимодействие.

image

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

Стоит отметить, что отдельные слои создаются также при ряде других условий. Если вспомнить о том, как и для чего появились слои, то о списке этих условий вполне можно догадаться — это все элементы предназначение которых активно изменяться и перерисовываться. Например: теги <video>, <canvas>, плагины Flash и Silverlight, css-фильтры и наши, оказавшиеся cтоль полезными, translate’ы. Вернёмся к ним.

Обратите внимание, перерисовка всё ещё происходит, причём дважды – в начале анимации и в конце. Допустим, это нас не устраивает, и мы хотим совсем избавиться от неё. Нам надо понять почему она возникает.

У хрома есть свойство show composite layers. Оно подсвечивает границы слоёв и позволяет увидеть какой элемент страницы вынесен в отдельный слой. Включим его, и вернёмся к нашему примеру.

image

Запустим анимацию.

image

Как мы можем видеть по оранжевой границе – блок был выделен в отдельный слой и передвинут. После чего границы растворились. Не трудно догадаться что произошло. Блок был вырван из общего слоя страницы (первый paint), слой с ним был передвинут, после чего снова была вызван paint, чтобы объединить слои.

Чтобы избавиться от двух лишних перерисовок, нам необходимо выделить блок в отдельный слой и предотвратить его слияния с другими. Это можно сделать при помощи transform: translate3d, заменив двухмерное преобразование на трёхмерное.

.elem{
    …
    translate: transform3d(0,0,0);
    …
}
.elem--active{
      translate: transform3d(400px,0,0);
}


image

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

Производительность существенно улучшилась. Вот как выглядит Timeline при использовании свойства left:

image

А вот при использовании transform:

image

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

У данного подхода, тем не менее, есть отрицательная сторона. Во-первых, 3d технологии в браузере всё ещё экспериментальная технология и до стабильности ей ещё далеко. Например, transform3d не поддерживается полностью в IE вплоть до последних версии. Поэтому, этот приём не годится, если у вас значительная часть пользователей пользуются этим браузером. Остаётся полагаться на transformX/Y у которого значительно большая поддержка.

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

Суммируя всё описанное выше:
  • Лишние и ненужные paint’ы плохо сказываются на производительности
  • Диагностировать наличие неоптимальных участков позволяют встроенные в хром инструменты разработчика
  • Избавить от лишних перерисовок можно при помощи translate:transform’ов
  • При оптимизации необходимо учитывать поддержку браузеров и пользователей мобильных устройств

Комментарии (14)


  1. chikuyonok
    28.05.2015 12:16
    +10

    У данного подхода, тем не менее, есть отрицательная сторона.

    Их гораздо больше :) И если их не учитывать — получим обратный эффект: сильное снижение производительности.

    1. Отдельный GPU-слой почти всегда занимает отдельную память, которую можно посчитать (в байтах) по довольно приблизительной формуле width ? height ? 4. То есть чем больше слоёв (и чем больше физические размеры слоя), тем больше расход памяти. Неумелым использованием слоёв можно очень быстро вырубить браузер на мобилках.
    2. В Webkit-браузерах (десктоп и мобильные) слой перерисовывается целиком. Это значит, что если где-то внутри слоя поменяется хотя бы один пиксель, браузер сделает repaint всего слоя, а также потратит время на перенос его в GPU. Хотя Blink и научился оптимизировать такие вещи, за этим всё равно нужно следить с помощью описанных в статье инструментов.
    3. Если GPU-слой по z-index будет ниже, то и этот слой тоже будет вынесен на GPU. Со всеми последствиям, описанными выше и с возможными артефактами отрисовки.


    Вообще стоит понимать, что GPU-слои (или compositing, как это называется в Blink) — это один большой хак, который сами разработчики браузеров пытаются от нас скрыть и заставить браузер работать так, будто никаких GPU и не-GPU слоёв нет. Поэтому нет никакого «официального» описания как этим правильно пользоваться. Более того, поведение браузеров постоянно меняется: например, Blink далеко не всегда принудительно вынесет слой на GPU даже если ему указать transform: translateZ(0), так он пытается оптимизировать работу с памятью и железом. Иногда это к лучшему, а иногда приходится искать варианты обхода этой оптимизации, чтобы браузер не делал лишний repaint перед анимацией.

    Поэтому когда собираетесь выносить что-то на GPU, нужно чётко представлять себе все возможные последствия и уметь правильно пользоваться инструментами для отладки. Я таким образом уже несколько сайтов спас от крэшей на мобильных устройствах :)


    1. SelenIT2
      28.05.2015 12:57
      +2

      Еще один минус выноса в GPU-слой — усложнение отладки. Например, тот же Хром толком не показывает затраты на перекомпозицию/перерисовку при чисто CSS-ных анимациях, но это не значит, что их нет.


      1. chikuyonok
        28.05.2015 13:26
        +3

        Вроде через chrome://tracing/ это можно отловить, хотя не так удобно, как через Timeline


    1. claustrofob
      28.05.2015 13:04

      Поэтому нет никакого «официального» описания как этим правильно пользоваться.


      Вообще-то есть. CSS свойство will-change позволяет сообщить браузеру об изменениях, которые будут применены к элементу.


      1. chikuyonok
        28.05.2015 13:19
        +3

        Да, есть такое свойство. С помощью которого вы всего лишь говорите браузеру «я планирую менять вот такие свойства, оптимизируй это как-нибудь». А как браузер это оптимизирует — вынесет отдельным слоем на GPU, склеит с другими блоками и вынесет отдельным слоем или вообще оставит на основном холсте — вы не знаете. Вы не можете с помощью этого свойства сказать «сразу вынеси этот слой на GPU, потому что потом я буду анимировать кучу блоков и мне не нужен лишний repaint перед стартом анимации; я даю отчёт всем своим действиям и принимаю на себя все возможные риски». Я очень долго на последнем визульано-сложном проекте искал возможность сделать именно так, и никакой will-change тут не помогал. Я пытался именно обойти браузерную оптимизацию и контролировать весь процесс самостоятельно для достижения нужного соотношения производительности и расхода памяти.


      1. SelenIT2
        28.05.2015 13:24
        +1

        Will-change — не более чем «декларация о намерениях». Какие конкретно оптимизации браузер предпримет (и предпримет ли вообще), остается на усмотрение самого браузера. Да, оно лучше, чем хаки типа translateZ(0) и -*-backface-visibilty:hidden (кстати, AFAIK, последний больше не работает в вебките), но фактически это всё равно хак, со своими побочными эффектами, порой до боли напоминающий старый (не)добрый hasLayout


    1. ianbrode Автор
      28.05.2015 13:27

      Спасибо за дополнение. :)

      GPU-слои (или compositing, как это называется в Blink) — это один большой хак


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


  1. TimsTims
    28.05.2015 15:20
    +3

    Оптимизация это конечно хорошо, но ничто не может быть лучше полного отказа от анимации:

    -Если изображение (в нашем случае веб-страница) не успевает отрисоваться
    Значит нафиг не надо использовать такую анимацию. не мучайте пользователя, не садите ему аккумулятор лишними расчетами. Большинство устройств, под которые вы оптимизируете код «чтобы везде работало» и так слишком слабые, чтобы отображать такую веб-анимацию. Как не оптимизируй, а всегда найдется более слабое устройство, на котором «будет тормозить». А на устройствах которые достаточно мощные для анимации — они даже и не почувствуют была ли тут оптимизация, или нет.

    Прошу за весь интернет: отключайте анимации! Уговаривайте руководство отказаться от анимации, Приводите доводы, Показывайте на разных устройствах (не только на айфонах 6). Если хоть где-то тормозит, то это будет минус х10 ваших потенциальных пользователей. Фух накипело… яркий пример «крутой анимации» — сайт альфы(на хабре уже обсуждали) alfabank.ru


    1. Kuzzy
      30.05.2015 01:39
      +4

      Оптимизация для слабаков, давайте жить в статичном, черно-белом мире, без анимаций :)

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


      1. TimsTims
        30.05.2015 08:03
        -2

        Хотите анимации — смотрите мультфильмы. Но не надо на сайт добавлять крутилки-вертелки, фейерверки. Большинство пользователей будет долго втыкать в интерфейс, вторые будут ждать пока всё отвиснет, так и не поняв что произошло, третие вообще ничего не увидят, т.к. у них браузер в телефоне просто завис. Веб не подходит для нормальной анимации, увы. Даже у того же ютуба слегка подлагивают элементы, а хочется чтобы всё было плавно как в обычных программах


  1. webdi
    02.06.2015 10:19
    +2

    У автора опечатка в коде статьи:
    translate: transform3d…
    вместо
    transform: translate3d


  1. Sliver
    02.06.2015 11:13
    -3

    Как прекрасно, что есть Firefox и NoScript.


    1. SelenIT2
      02.06.2015 11:39
      +1

      От CSS-анимаций не поможет:)


      1. Sliver
        02.06.2015 11:54
        -2

        Как хорошо, что мало кто умеет делать css-анимации :)