Как стать автором
Обновить
447.18

7 + 1 способ анимировать спиннер

Время на прочтение6 мин
Количество просмотров11K

Меня зовут Евгений Подивилов, я фронтенд-разработчик в команде «Лайфстайл». Я разрабатываю раздел «Развлечения». В этом разделе можно купить билеты на мероприятия или забронировать столик в ресторане.

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

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

С чего мы начинали

Однажды при открытии нескольких SPA-приложений разом топовый Макбук завыл. Мы провели расследование и выяснили, что причиной стал обычный спиннер:

Оказалось, что на него тратилось почти 10% ресурсов CPU! Мне стало интересно разобраться и все оптимизировать, и я погрузился в код.

Все замеры производились на конфигурации: Google Chrome версии 95.0.4638.54; macOS Big Sur 11.6; MacBook Pro 15 Mid 2015, Intel Core i7 2.2Ghz, 16Gb DDR3.

Решение первое: React

Я начал с решения на React. Каждый цикл анимации вычислял значения, устанавливал их в стейт компонента, а затем использовал в render-функции. Каждые 16 мс запускались те механизмы React, за которые разработчики его так «любят». Не самое хорошее решение.

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

Цикл анимации на React
Цикл анимации на React

Кроме проблем с лишней работой бывают проблемы с потреблением памяти. График показывает сначала рост, а затем обрыв — освобождение неиспользуемой памяти. И такое происходит часто.

Потребление памяти на React
Потребление памяти на React

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

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

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

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

Подробнее про GC на примере Chrome можно прочитать на v8.dev.

Решение второе: SVG + JS

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

Теперь на вкладке Performance цикл анимации выглядит проще, но не значительно лучше: self time задачи уменьшилось с 1,55 до 0,17 мс, но общее время снизилось только до 4,69 мс против 5,69 мс в версии с React.

Цикл анимации на SVG + JS
Цикл анимации на SVG + JS

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

Потребление памяти SVG + JS
Потребление памяти SVG + JS

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

С помощью JS меняем стили элемента path, что вызывает этап style. За ним вызывается layout, дальше paint и composite. Можно оптимизировать этот пайплайн и пропустить некоторые шаги, если использовать свойства, подходящие для анимаций. Например, пропустить этапы layout и paint, если анимация основана только на свойстве transform.

Сделать такую анимацию только на transform, кажется, не получится? "Чтобы сделать анимацию быстрой, нужно все писать на Canvas!" - подумал я.

Решение третье: Canvas

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

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

Цикл анимации на Canvas
Цикл анимации на Canvas

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

Есть возможность распараллелить выполнение задач через web workers. Но в них нет доступа к DOM, а значит, мы не сможем менять состояния элементов. И я решил избавиться от JS. 

Решение четвертое: SVG + CSS

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

Результаты улучшились, но проблема с фризами анимации осталась. Не все свойства CSS анимируются в отдельном потоке. В CSS можно анимировать с высокой производительностью только два свойства — transform и opacity. Зато с памятью теперь проблем нет.

Цикл анимации на SVG + CSS
Цикл анимации на SVG + CSS
Потребление памяти на SVG + CSS
Потребление памяти на SVG + CSS

Решение пятое: SVG

Я отказался от JS, получится ли отказаться и от CSS?

SMIL — это Synchronized Multimedia Integration Language, такой HTML для анимаций. Можно запрограммировать анимации в виде разметки с помощью набора тегов и атрибутов. Можно изменить код так, что вся анимация будет только в SVG-файле.

Без JS. Без CSS. Работает практически везде, кроме IE. Такую анимацию можно подключить с помощью тега <img> или свойства background-image. Но блокировка основного треда по-прежнему блокирует нашу анимацию.

Решение шестое: Video

Видео кажется отличной идеей и имеет ряд преимуществ:

  • расчеты всех этапов анимации выполняются заранее и «зашиваются» в видео,

  • поддерживается всеми браузерами,

  • существуют аппаратные оптимизации для некоторых видео кодеков.

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

<video width="100%" height="100%" autoplay loop muted playsinline>
  <source src="spinner.mov" type='video/mp4; codecs="hvc1"' />
  <source src="spinner.webm" type="video/webm" />
</video>

Но есть и несколько минусов:

  • нужен прозрачный фон. Даже если не надо поддерживать IE 11, все равно нужно минимум два варианта видео: с кодеком VP9 — для Chrome и HEVC — для Safari;

  • менять размеры видео без потери в качестве не получится. Значит, нужно несколько видеофайлов под каждый размер спиннера;

  • перезапуск видео выполняется в основном потоке, хотя само видео выполняется в отдельном. Спиннер будет крутиться, пока не проиграет один цикл анимации.

Решение седьмое: CSS

Но если подумать, что такое видео? Набор кадров, которые очень быстро переключаются. Что, если использовать именно это качество в CSS?

Звучит как бред, но раз я решил пробовать все варианты, то почему бы и нет. С помощью черной магии ffmpeg разложил видео покадрово, а с image magic собрал атлас из полученных кадров. Главное, чтобы фон был прозрачным. С помощью простых и быстрых CSS-трансформаций я менял кадры:

Сработало! Даже при условии, что основной поток занят JS. Да, по-прежнему есть проблемы с ресайзом, но их можно решить с помощью нескольких атласов. Мы даже можем подгружать атлас на сайт с использованием lazy-атрибута. <sarcasm>Кажется, я нашел идеальное решение.</sarcasm> 

Выводы

Кроме разных способов анимации я хотел показать, что в большинстве случаев нам достаточно простых, но быстрых решений. Даже создатели популярной библиотеки компонентов material-ui пошли по пути сокращения потребления ресурсов и в пользу UX.

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

Решения рабочие, но использовать их в продакшене крайне сомнительно. Для обычного спиннера слишком много затрат. В итоге нам удалось убедить дизайнеров и бизнес, что будет лучше заменить наш вычурный индикатор на что-то попроще. Например, такое:

Результаты профилирования решений

Теги:
Хабы:
Всего голосов 32: ↑32 и ↓0+32
Комментарии16

Публикации

Информация

Сайт
l.tbank.ru
Дата регистрации
Дата основания
Численность
свыше 10 000 человек
Местоположение
Россия