Хабрахабр

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

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

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

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

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

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

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

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

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

image

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

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

image

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

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

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

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

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

image

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

Если по ней пройти, то можно будет увидеть, что в этом примере идет цикл по всем пикселям на экране. Если нажать на блок, помеченный как 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.

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

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

Canvas и шейдеры

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

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

image

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

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

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

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

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

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

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

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

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

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

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

image

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

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

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

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

image

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

@keyframes rotate 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

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

Заключение

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

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»