Производительность анимаций на сайтах

image

При разработке сайтов, выходящих за рамки условного бутстрапа, рано или поздно возникают вопросы, связанные с производительностью анимаций. Особенно важными они являются в дизайнерских сайтах, вроде тех, что попадают в каталоги Awwwards, FWA, CSS Design Awards и.т.д. При этом часто задача создания анимаций и последующей оптимизации, если она будет нужна, ложится на плечи не очень опытных разработчиков, которые даже не знают с чего начать. Обычно все это выливается в тормозящие сайты, которыми невозможно пользоваться, и последующее негативное отношение ко всему классу таких проектов. В этой статье мы постараемся разобрать, где находится граница приемлемой производительности анимаций, какие узкие места часто встречаются и куда смотреть в инструментах разработчика в первую очередь.

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


Как браузер отображает страницу

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


  1. Style calculation (браузер разбирает CSS-селекторы, определяет какие стили к чему нужно применять)
  2. Layout creation (собственно формируется макет страницы)
  3. Painting (создаются пиксельные представления элементов для последующего рендеринга)
  4. Layer composition (браузер собирает все воедино и показывает на экране)

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

Узкие места каждого из этих шагов мы рассмотрим дальше, а сейчас зададимся одним глупым на первый взгляд вопросом, с которого по идее нужно начинать…


Тормозит или не тормозит, вот в чем вопрос…

Очень часто можно встретить людей, которые ничего не делают с явно тормозящим сайтом и говорят «а у меня page speed дает 100 баллов, все хорошо». Или наоборот, на хорошо работающем сайте люди долго занимаются какими-то оптимизациями, потому, что какой-то алгоритм работает неэффективно по каким-то загадочным метрикам. Но между этими крайностями должна быть середина здравого смысла, так где же она?

image

Чтобы познать дзен понять, нужна ли оптимизация ваших анимаций, нужно осознать глубокую философскую мысль:


Если ты видишь, что сайт тормозит, значит он тормозит. Если ты не видишь, что сайт тормозит, значит от не тормозит.

Многие люди почему-то считают это утверждение очень глупым, но так ли это? Для конечного пользователя производительность — это не какие-то метрики или идеальные алгоритмы со строгим математическим обоснованием. Для него производительность — это одно из двух: тормозит или не тормозит.

Как он это определяет? Глаз человека, проводящего много времени за монитором, начинает резко реагировать на падение fps. Это вызывает странное чувство дискомфорта. Соответственно наша задача, как разработчиков, не допускать проседания. Пользователь привык видеть работу браузера в 60fps? Хорошо, значит делаем все, чтобы все так и оставалось. Берем ноутбук со средним железом и смотрим. Видим сильно меньше 60fps — оптимизируем. Видим около 60 — ничего не трогаем. Пользователь разницы все равно не заметит, а мы потратим кучу времени на оптимизации ради оптимизаций.


Не занимайтесь оптимизациями ради оптимизаций.


16.5ms

Выражаться в терминах fps не удобно, так что перейдем к миллисекундам. Нехитрым делением 1000ms / 60fps получаем, что на один кадр приходится примерно 16.5ms времени.

Что это означает? За 16.5ms браузер должен отобразить нам текущее состояние страницы с анимацией, пройдя по шагам, которые мы смотрели выше, и при этом должны остаться ресурсы на работу других скриптов, общение с сервером и.т.д. Если на отображение текущего состояния страницы будет тратиться большее время — мы увидим глазами лаг. Если около 16ms, то проседания не будет, но вполне вероятно, что загрузка железа будет очень высокой, кулеры будут гудеть, а телефоны греться. Таким образом нам нужно следить за тем, чтобы отрисовка одного кадра не приближалась к этому значению по времени, а еще лучше не была больше 10ms, чтобы оставался запас по производительности. Не забывайте также, что тесты проводятся всегда на среднем железе — например в последующих примерах скриншоты будут делаться на Pentium Silver со встроенной графикой.


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

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


Инструменты разработчика в Google Chrome

Многих верстальщиков инструменты разработчика в браузере пугают чуть ли не сильнее, чем консоль linux. Но на самом деле там нечего бояться. Да, там много кнопок, но они избыточны для решения наших задач. Сейчас мы посмотрим, куда стоит обращать внимание в первую очередь, чтобы понять, что делать с анимацией, и нужно ли вообще что-то делать.

Когда речь идет о производительности, мы большую часть времени будем проводить во вкладке performance и будем нажимать одну и ту же кнопку.

image

Сочетание клавиш Ctrl-E или круглая кнопка слева запускают и останавливают запись происходящего. Результаты выводятся здесь же. Браузер записывает много всего, но лучше один раз увидеть, чем много раз прочитать, так что возьмем какую-нибудь анимацию и посмотрим на нее. Пусть для начала это будет простая CSS-анимация. Если открыть ее на весь экран, то можно будет увидеть, что работает она с заметными подлагиваниями:

Запишем несколько секунд в полноэкранном режиме и посмотрим, что там происходит:

image

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

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

Дальше мы видим несколько строчек, в которых разными цветами отображается нагрузка — можно посмотреть, сколько времени браузер потратил на разные виды деятельности. У нас анимация равномерная и для каждого кадра выполняются одни и те же операции, обозначенные фиолетовым и зеленым цветом. Если навести мышь на цветные блоки, то станет ясно, что мы имеем дело с теми пунктами, которые упоминали в начале — recalculate style и update layer tree — фиолетовые, а paint и composite layers — зеленые.

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

Вы можете заметить, что добавились желтые блоки, отображающие выполнение скриптов. Если там будет много вызовов функций, то для каждого вызова будет добавляться еще один блок — по их размеру нетрудно найти самую «тяжелую» функцию, с которой, вероятно, и стоит начать оптимизацию.

image

В примере время, затраченное на один кадр, колеблется в районе 80ms. Да что уж там, даже невооруженным глазом хорошо видно, как все подлагивает. Посмотрев в раздел summary внизу, мы видим, что больше всего времени занимают скрипты. По сравнению с ними rendering и painting выглядят как погрешности, которыми можно пренебречь. Не всегда, конечно, так бывает, но довольно часто.

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


Что делать, если…

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


Style calculation

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


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

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


Упрощайте селекторы в CSS, используйте БЭМ.


Layout creation

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

Главное правило, которым мы руководствуемся при создании анимаций — не допускать перестройку макета любой ценой. Поэтому обычно мы не пытаемся оптимизировать ее (да и возможностей особо нет), а именно стараемся ее избежать.

Свойств, которые могут вызывать перестроение макета много, вы можете найти списки в интернете, например на csstriggers.com есть неплохой. Чаще других в анимациях можно встретить свойства:

display
position / top / left / right / bottom
width / height
padding / margin
border
font-size / font-weight / line-height
и.т.д.

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


Не изменяйте геометрические свойства элементов, лучше используйте transform и opacity.

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


Не меняйте background элементов.

В некоторых браузерах (не буду тыкать пальцем в Firefox) может появляться характерный лаг CSS-анимаций с трансформациями, особенно если выполняется больше одной анимации в единицу времени. Внешне это может выглядеть не только как паузы в ее работе, но и как «срывы» анимации в самое свое начало. Кажется, что браузер постоянно что-то рассчитывает заново. Такое поведение почти всегда поправляется с помощью свойства backface-visibility.


По возможности добавляйте backface-visibility: hidden анимируемым элементам.

Также перестройку макета вызывают наши обращения к элементам из скриптов. Причем это не обязательно должно быть прямое изменение CSS, это может быть и обращение к некоторым свойствам и методам элементов. Наиболее часто встречаются:

offset***
client***
inner***
scroll***

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


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


Painting и layer composition

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

Браузер готовит пиксельное изображение страницы не целиком, а по частям — слоям. Их может быть много. Каждый слой существует как бы сам в себе и не затрагивает остальные, что создает почву для некоторых CSS-хаков. Но мы поговорим о них в другой раз. Затем из этих слоев собирается конечное изображение. В контексте анимаций очень полезно вынести анимируемые элементы в отдельный слой, чтобы их изменения не затрагивали все вокруг. Желательно, чтобы содержимое элементов было небольшим. Мы можем это сделать с помощью свойства will-change или, как это раньше делали, transform: translateZ (0). Единственное, что нужно помнить — это то, что нельзя увеличивать количество слоев бесконечно. В какой-то момент это сыграет злую шутку и производительность напротив упадет. Так что здесь будет два совета:


Используйте will-change или transform: translateZ (0), чтобы выносить анимируемые элементы в отдельный слой.

но в то же время


Не переусердствуйте с этим делом. Проверяйте в инструментах разработчика, что не стало хуже.

Очень часто серьезные проблемы вызывают фильтры, которые как-то трансформируют изображение элементов. Это могут быть простые CSS-фильтры с blur или замороченные варианты с SVG, но эффект будет одинаковым — заметное снижение производительности.


Не используйте сложные фильтры. Если все же нужен задуманный эффект — рассмотрите вариант реализации его на WebGL.


Насколько эти советы работают?

Работают, но не нужно ждать от них чуда. В сети новички иногда говорят «я добавил will-change, но ничего не изменилось». Обычно это значит, что основная проблема была в другом месте, а этот прием дал настолько маленький прирост производительности, что он остался незамеченным. Именно поэтому важно использовать инструменты разработчика, чтобы четко понимать, где именно находится узкое место и не тратить время и силы на попытки оптимизировать то, что и так работает нормально.

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


Скрипты…

Знаете откуда растут проблемы с тормозящими анимациями чаще всего (по моим наблюдениям)? Вот из этого подхода к разработке:

image

Звучит глупо, но так оно и есть. Постоянно встречаются решения, явно откуда-то скопированные совершенно без понимания, что там к чему. Бывает даже такое, что можно половину кода удалить и все продолжит работать. Часто код в ответах на SO или Тостере не предназначен для вашего продакшена. Это должно быть очевидно. Он показывает идею, отвечает на вопрос, но совершенно не является оптимальным конечным вариантом под вашу конкретную задачу.


Если уж копируете, то хотя бы просматривайте код на предмет лишних действий.


RequestAnimationFrame

Про этот метод часто говорят и рекомендуют использовать его вместо setTimeout / setInterval в анимациях. В этом есть смысл, так как те методы имеют свойство рассинхронизироваться по времени с кадрами, которые перерисовывает браузер и в результате получаются небольшие лаги. Но тут есть два замечания.

Во-первых, если на странице анимируется больше одного элемента и мы будем вызывать requestAnimationFrame много раз, то это приведет к резкому проседанию fps. В теории так быть не должно, но на практике все происходит именно так. Ознакомиться с тестами можно здесь.


Объединяйте все коллбеки для анимаций в один requestAnimationFrame.

Второй момент связан скорее с ситуацией, когда у нас уже есть тяжелая анимация, возможно с применением канваса, от которой мы не можем избавиться или времени нет переделывать, и происходит следующее: допустим анимация должна выполняться за N секунд и мы уже используем requestAnimationFrame. Но требуется много ресурсов для расчета текущего состояния и мы видим такую картину: анимация работает плавно, красиво, но за 2N, а то и 3N секунд. В результате все воспринимается оооччччеееннь меееедддллеенным. Для того, чтобы хоть как-то поправить такое поведение, можно пойти против всех рекомендаций, воспользоваться тем самым setInterval / setTimeout и привязать состояния анимируемых элементов к физическому времени, а не к абстрактным кадрам. В результате мы получим формальное уменьшение fps, но с психологическим эффектом прироста производительности.


В случае крайне медленной анимации может иметь смысл отказаться от requestAnimationFrame в пользу setInterval / setTimeout.


Canvas и шейдеры

Значительная часть анимаций на нестандартных сайтах связана с канвасом. Это вполне объяснимо, CSS — штука ограниченная, а здесь мы можем реализовать любые фантазии дизайнера. Но нужно иметь в виду, что обычный 2d-канвас — это далеко не самая производительная технология. Если вы начнете рисовать на нем много элементов или работать с пикселями напрямую, то быстро столкнетесь с тем, что fps проседает, или совершенно внезапно painting и layer composition начинают занимать неприлично много времени. Наглядно эту проблему можно увидеть в примере:

Посмотрим на то, что делает браузер (последний Google Chrome под линуксом):

image

Обратите внимание на то, насколько сильно разросся шаг layer composition. Это выглядит немного нелогично, там ведь только один элемент, что там можно так долго компоновать? Но при использовании 2d-канваса такое поведение — не редкость, и что-то с этим сделать очень проблематично. Это одна из причин, почему обычно мы склоняемся к использованию WebGL, там таких вопросов не возникает.


Если стоит выбор между 2d-канвасом и WebGL, выбирайте второе. Это даст изначальный бонус в производительности на тех же самых задачах.

С чем обычно ассоциируется WebGL? С шейдерами. А отладка шейдеров — это головная боль для любого, кто с ними работает. И инструменты разработчика тут практически бессильны. Обычно, если в шейдерах слишком много вычислений, мы видим в сводке внизу, что больше всего времени занимает «простой», который по факту является выполнением наших шейдеров независимо от браузера, и никаких полезных подробностей мы получить не можем.

Есть разные рекомендации о том, какие функции предпочитать другим в шейдерах, потому, что они якобы лучше оптимизированы. Или что нужно избегать блокирующих операций. Это все верно, но по моим наблюдениям в большинстве случаев шейдеры, которые слишком тормозят работу сайта — это просто очень большие шейдеры. Если вы написали 100 строк GLSL в одном месте, это почти гарантированно будет плохо работать. А если там еще и разные вложенные конструкции, циклы, то все — пиши пропало. Дать какие-то рекомендации здесь сложно, разве что:


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

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

В связи с этой мыслью вспоминается «болезнь», которой особенно подвержены бывшие олимпиадники. Она почему-то сильно проявляется именно при работе с канвасом. По ее причине стоит всегда осторожно копировать код таких людей. Они стараются использовать «правильные» математические алгоритмы, сложные физические формулы, рассчитывать все движения элементов с большой точностью даже там, где это совершенно ни к чему. Это приводит к росту нагрузки на процессор и к тому, что за наши условные 10ms он ничего не успевает посчитать. На практике же часто можно обойтись приближенными формулами и школьными знаниями физики. Не нужно все усложнять, мы делаем сайты, а не программное обеспечение для баллистических ракет.


Используйте простые алгоритмы.

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


С самим шейдером можно ознакомиться здесь.

На практике это все требует невероятных ресурсов для работы. В полноэкранном режиме мы имеем 400–800ms на кадр (а вообще в этом примере и до 1500ms может подниматься):

image

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


Не используйте RayMarching, это верный способ убить производительность.


Практический пример

В статьях про производительность часто не хватает примеров, а на слово поверить бывает сложно. Так что рассмотрим пару. Помните первый пример с вращающимся туннелем на CSS? Браузер делал много всего:

image

Мы хотим немного ускорить все это. С чего начать? Мы видим фиолетовые блоки, это значит, что браузер постоянно перестраивает макет. Скриптов там нет, но есть CSS анимации, в которых что-то меняется. Посмотрим на их код:

@keyframes rotate {
    from {
        transform: rotate(0);
    }
    to {
        transform: rotate(360deg);
    }
}

@keyframes move-block {
    from {
        transform: translateX(0);
        background: @color1;
    }
    to {
        transform: translateX(-@block-size * 6);
        background: @color2;
    }
}

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

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

Посмотрим, что делает браузер:

image

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


Еще пример

Вспомним, как выглядела работа браузера в генераторе шума:

image

Проблема определенно в скриптах. Видно, что блок «render» самый большой. Это наша основная функция для отрисовки изображения. Посмотрим на нее:

function render() {
    let imageData = CTX.createImageData(CTX.canvas.width, CTX.canvas.height);

    for (let i = 0; i < imageData.data.length; i += 4) {
        const color = getRandom();
        imageData.data[i]     = color;
        imageData.data[i + 1] = color;
        imageData.data[i + 2] = color;
        imageData.data[i + 3] = 255;
    }

    CTX.putImageData(imageData, 0, 0);

    requestAnimationFrame(render);
}

Здесь определенно идет работа с отдельными пикселями. Это не очень здорово. Мы говорили, что по возможности лучше использовать не 2d-канвас, а WebGL, а эта задача так и хочет, чтобы ее распараллелили с помощью шейдера. Сделаем это:

Что в итоге получится? Смотрите сами:

image

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


Заключение

В этой статье мы разобрались с тем, когда стоит заниматься оптимизацией производительности анимаций, как пользоваться инструментами разработчика в Chrome в этом контексте и на что обращать внимание в первую очередь. Надеюсь эта информация будет полезна разработчикам, которые впервые столкнулись с такими задачами и не знают с чего начать.

© Habrahabr.ru