В трендах дизайна уже давно засели красивые анимации. UI-дизайнеры делают выверенные «карусели», загрузки, анимации меню и другие украшения, а frontend разработчики переводят их в код. Но сайт должен не только хорошо выглядеть, но и быстро работать.
Современный «фронтенд» должен оптимизировать свой код. Особенно это актуально для продуктов, у которых большая часть аудитории переходит на сайт с мобильных устройств. Некоторые методы анимации лагают даже в «Хроме» на топовых компьютерах, а должны плавно работать на среднем смартфоне.
Наши разработчики пользовались большим количеством приемов, которые помогли оптимизировать сайт и ускорить его работу. Я собрал 4 самых интересных из них. Мы делимся знаниями, которые пригодятся новичкам и профессионалам, а также даем ссылки на полезные туториалы.
1. Анимация на SCSS
На сайте много анимации. Нам нужно, чтобы браузер проигрывал ее со стабильным фреймрейтом 60 fps. На чистом CSS это сделать сложно, поэтому мы пользуемся SCSS.
Для создания слайдеров мы использовали библиотеку Swiper. Для горизонтального слайдера применение этой библиотеки обоснованно, так как нам нужно было предусмотреть поддержку свайпов со стороны пользователя. Но для вертикальной бесконечной карусели Swiper не нужен, здесь нет взаимодействия с пользователем. Поэтому мы хотели повторить тот же функционал, используя только возможности CSS.
Первое и главное условие при работе с CSS-анимацией — это использование только свойств transform и opacity. Браузеры умеют самостоятельно оптимизировать анимацию этих свойств и выдавать стабильные 60 fps. Однако, с помощью одного @keyframes невозможно написать разную анимацию для разных элементов, а анимировать каждый элемент индивидуально на чистом CSS слишком трудоемко. Как быстро написать нужную нам анимацию? Мы выбрали SCSS, диалект SASS — более функциональное расширение CSS.
Разберем использование SCSS на примере работы нашего вертикального слайдера.
В нашем распоряжении есть контейнер, высота которого равна высоте трех элементов карусели. Внутри него расположен еще один контейнер, содержащий в себе все элементы карусели.
<div class="b-vertical-carousel-slider">
<div class="vertical-carousel-slider-wrapper slider-items">
<div class="vertical-carousel-slider-item"></div>
<div class="vertical-carousel-slider-item"></div>
<div class="vertical-carousel-slider-item"></div>
<div class="vertical-carousel-slider-item"></div>
<div class="vertical-carousel-slider-item"></div>
</div>
</div>
Мы убираем видимость элементов контейнера, которые будут выходить за его пределы и задаем высоту блокам.
.b-vertical-carousel-slider {
position: relative;
overflow: hidden;
height: $itemHeight * 3;
.vertical-carousel-slider-item {
height: $itemHeight;
}
}
Расчет анимации меняется, только если меняется количество элементов в карусели. Далее мы пишем миксин, который принимает один параметр на вход — $itemCount
@mixin verticalSlideAnimation($itemCount) {
}
В миксине генерируем keyframe для каждого элемента, задаем ему начальное состояние и с помощью :nth-child
определяем элементу анимацию.
for $i from * 1 through $itemCount {
$animationName: carousel-item-#{$itemCount}-#{$i};
@keyframes #{$animationName} {
0% {
transform: translate3d(0, 0, 0) scale(.95);
}
}
.vertical-carousel-slider-item:nchild(#{$i}) {
animation: $stepDuration * $itemCount $animation ease infinite;
}
}
Дальше при анимации мы будем перемещать элементы только по оси y и менять scale для элемента в центре.
Состояния элемента карусели:
- Покой
- Смещение по оси y
- Смещение по оси y с увеличением
- Смещение по оси y с уменьшением
Каждый элемент будет двигаться вверх $itemCount
раз, один раз увеличиваться и один раз уменьшаться во время движения. По этому мы сгенерируем и рассчитаем выполнение анимации для каждого из движений вверх.
@keyframes #{$animationName} {
0% {
transform: translate3d(0, 0, 0) scale(.95);
}
@for $j from 0 through $itemCount {
$isFocusedStep: $i == $j + 2;
$isNotPrevStep: $i != $j + 1;
$offset: 100% / $itemCount * ($animationTime / $stepDuration);
@if ($isFocusedStep) {
#{getPercentForStep($j, $itemCount, $offset)} {
transform: getTranslate($j - 1) scale(.95);
}
#{getPercentForStep($j, $itemCount)} {
transform: getTranslate($j) scale(1);
}
#{getPercentForStep($j + 1, $itemCount, $offset)} {
transform: getTranslate($j) scale(1);
}
#{getPercentForStep($j + 1, $itemCount)} {
transform: getTranslate($j + 1) scale(.95);
}
} @else if ($isNotPrevStep) {
#{getPercentForStep($j, $itemCount, $offset)} {
transform: getTranslate($j - 1) scale(.95);
}
#{getPercentForStep($j, $itemCount)} {
transform: getTranslate($j) scale(.95);
}
}
}
}
Здесь осталось определить некоторые переменные и функции:
$animationTime
— время анимации движения$stepDuration
— общее время выполнения одного шага анимации ($animationTime
+ время покоя карусели)getPercentForStep($step, $itemCount, $offset)
— функция, возвращающая в процентах крайнюю точку одного из состояний.getTranslate($step)
— возвращает translate в зависимости от шага анимации
Примерные имплементации функций:
@function getPercentForStep($step, $count, $offset: 0) {
@return 100% * $step / $count - $offset;
}
@function getTranslate($step) {
@return translate3d(0, -100% * $step, 0);
}
У нас есть работающая карусель с увеличивающимся в середине элементом. Осталось сделать тень под увеличивающимся элементов. Первоначально каждый элемент карусели имел псевдоэлемент :after, который в свою очередь имел тень. Чтобы не анимировать свойство shadow, мы использовали для него свойство opacity, т.е. просто показывали и скрывали тень.
Но в новой реализации для такого решения нужно сгенерировать много дополнительных кейфреймов для каждого псевдоэлемента. Мы решили поступить проще: блок с тенью будет один и он будет занимать пространство ровно под средним элементом карусели.
Добавляем div, отвечающий за тень.
<div class="b-vertical-carousel-slider">
<div class="vertical-carousel-slider-wrapper">
<div class="vertical-carousel-slider-item"></div>
<div class="vertical-carousel-slider-item"></div>
<div class="vertical-carousel-slider-item"></div>
<div class="vertical-carousel-slider-item"></div>
<div class="vertical-carousel-slider-item"></div>
</div>
<div class="vertical-carousel-slider-shadow"></div>
</div>
Cтилизуем его и добавляем анимации.
@keyframes shadowAnimation {
0% {
opacity: 1;
}
80% {
opacity: 1;
}
90% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.vertical-carousel-slider-shadow {
top: $itemHeight;
left: 0;
right: 0;
height: $itemHeight;
animation: $stepDuration shadowAnimation ease infinite;
}
В этом случае не нужно генерировать и придумывать способ анимации тени под каждым элементов, мы анимируем лишь один блок, он имеет два состояния — виден и скрыт
В итоге у нас есть карусель на чистом CSS, которая анимируется только хорошо оптимизируемыми свойствами. Это позволяет браузеру использовать аппаратное ускорение для рендеринга. Отсюда ощутимый профит по сравнению с JS-анимацией:
- Во время скролинга страницы с анимацией на слабых устройствах в JS-анимации явно пропускались кадры, «роняя» FPS до 15-20. CSS-анимация явно улучшила состояние дел. На этих же устройствах этот показатель составлял минимум 50-55 FPS.
- Мы избавились от работы стороннего модуля там, где это не требовалось.
- Анимация будет проигрываться даже при отключенном JS
JS-анимация должна использоваться, если нужен строгий контроль над каждым кадром: пауза, реверсивное воспроизведение, перемотка, реакция на пользовательские действия. В остальных случаях рекомендуем пользоваться чистым CSS.
Полезные ссылки
2. Использование Intersection Observer API
Анимация, которую не видит посетитель сайта, проигрывается где-то вне поля зрения и нагружает CPU. С помощью Intersection Observer мы определяем, какую анимацию видно на экране прямо сейчас, и проигрываем только ее.
Все приемы из прошлого пункта можно комбинировать с Intersection Observer. Этот инструмент помогает не нагружать браузер анимацией, которую посетитель сайта не видит. Раньше, чтобы понять, смотрит ли посетитель на анимированный элемент, использовали ресурсоемкие «слушатели» событий и это не давало сильного «выхлопа». Разница между использованием анимации вне viewport и использованием «слушателей» была минимальной. Intersection Observer API требует меньше ресурсов и помогает проигрывать только ту анимацию, которую видно посетителю.
На нашем сайте анимация активируется только при появлении элемента во viewport. Если бы мы этого не сделали, то страницы были бы перегружены постоянным выполнением цикличных анимаций, оставшихся за пределами видимости. Intersection Observer API позволяет следить за пересечением элемента с родителем или областью видимости документа.
Пример реализации
Для примера покажем, как оптимизировать анимацию на JS. Идея простая — анимация проигрывается, пока анимируемый элемент находится во viewport. Для реализации мы используем Intersection Observer API.
Добавим в стили обработку класса is-paused
.b-vertical-carousel-slider.is-paused {
.vertical-carousel-slider-wrapper {
.vertical-carousel-slider-item {
animation-play-state: paused;
}
}
.vertical-carousel-slider-shadow {
animation-play-state: paused;
}
}
Т.е. при появлении этого класса анимация будет поставлена на паузу.
Теперь опишем логику добавления и удаления этого класса
if (window.IntersectionObserver) {
const el = document.querySelector('.b-vertical-carousel-slider');
const observer = new IntersectionObserver(intersectionObserverCallback);
observer.observe(el);
}
Здесь мы создали экземпляр IntersectionObserver, указали функцию intersectionObserverCallback
, которая будет срабатывать при изменении видимости.
Теперь определим intersectionObserverCallback
function intersectionObserverCallback(entries){
if (entries[0].intersectionRatio === undefined) {
return;
}
helperDOM.toggleClass(el, 'is-paused', entries[0].intersectionRatio <= 0);
};
Теперь проигрывается анимация только на тех элементах, которые видны. Как только элемент пропал из поля зрения, анимация ставится на паузу. Когда посетитель вернется к нему, воспроизведение продолжится.
Полезные ссылки
- Браузеры, которые поддерживают Intersection Observer
- Для браузеров, которые его не поддерживают, нужно использовать полифил
3. Рендеринг SVG
Большое количество изображений или использование спрайтов вызывает фризы и лаги в первые секунды после загрузки. Встраивание SVG в код страницы помогает визуально сделать загрузку более плавной.
Когда мы подбирали методы работы с изображениями, у нас было 2 варианта оптимизации: встраивание SVG в HTML или использование спрайтов. Мы остановились на встраивании. Мы вставляем XML-код каждого изображения прямо в HTML-код страниц. Это немного увеличивает их размер, зато SVG подается inline, сразу с документом.
Многие разработчики продолжают пользоваться SVG-спрайтами. В чем суть метода: массив изображений (например, иконки), собираются в большое изображение-полотно, которое и называется спрайтом. Когда нужно показать конкретную иконку, вызывается спрайт, после чего даются координаты определенного куска, на котором оно находится. Так делали давно, еще на первой версии HTTP. Спрайты помогали агрессивно кэшировать файл и уменьшить количество запросов на сервер. Это было важно, потому что много одновременных запросов тормозили браузер. Использование SVG-спрайтов — это типичный костыль, с которым вы пренебрегаете логикой работы ради экономии ресурсов. Сейчас количество запросов не так важно, поэтому мы рекомендуем встраивание.
В первую очередь оно положительно влияет на производительность с точки зрения посетителя. Он видит, как иконки моментально загружаются и не страдает первые секунды после загрузки страницы. При использовании спрайта или PNG-изображений страница, которая грузится, немного подтормаживает. Особенно сильно это ощущается, если посетитель сразу скроллит загруженную страницу — FPS будет падать до 5–15 на нетоповых устройствах. Встраивание SVG в HTML помогает сократить время ожидания загрузки страницы (субъективное, с точки зрения клиента) и избавиться от фризов и пропусков кадров при загрузке.
4. Кэширование с использованием Service Worker и HTTP Cache
Нет смысла повторно загружать страницу, которая не изменилась, и использовать трафик посетителя. Есть много стратегий кэширования, мы остановились на самой эффективной связке.
Оптимизировать стоит использование не только CPU/GPU, но и сети. Мобильные устройства — это ограничение не только по ресурсам, но и по скорости интернета и трафику. Здесь нам помогло кэширование. Оно позволяет сохранить ответы на HTTP-запросы и использовать их без повторного получения ответа от сервера.
Когда мы обдумывали стратегию кэширования, то выбрали одновременное использование Service Worker и HTTP Cache. Начнем с первого и более продвинутого. Service Worker — это js-файл, который может контролировать свою страницу или файл, перехватывать и модифицировать запросы, а также программируемо кэшировать запросы. Он работает как прокси-сервер между сайтом и сервером и определяет их офлайн-поведение. Все это делается на «фронте», без подключения «бэкенда».
Service Worker имеет огромную вариативность. Мы можем программировать поведение так, как нам угодно. Например, мы знаем, что посетитель, который перешел на страницу №1, с вероятностью 90% перейдет на страницу №2. Мы просим SW фоном подгрузить вторую страницу, когда посетитель находится еще на первой. Когда он перейдет на нее, страница загрузится моментально. Его можно использовать для разных задач:
- Фоновая синхронизация данных
- Офлайн-работа калькуляторов
- Кастомная шаблонизация
- Реакция на определенное время и дату.
Файлы Service Worker можно сделать в разных сервисах. Мы рекомендуем Workbox. Он достаточно простой и позволяет создавать свод правил, по которым ведется кэширование, например, precache.
Service worker поддерживают не все браузеры, например, IE, Safari или Chrome до версии 40.0. Если устройство не может с ним работать, оно выполняет правила кэширования HTTP Cache. Мы добавили страницам следующие HTTP заголовки:
cache-control: no-cache
last-modified: Mon, 06 May 2019 04:26:29 GMT
В этом случае браузер добавляет в хранилище ответы на запросы, но при каждом последующем запросе отправляет заголовок для проверки наличия изменений.
if-modified-since: Mon, 06 May 2019 04:26:29 GMT
В случае, если изменений не произошло, браузер получает ответ с кодом 304 Not modified и использует контент, сохраненный в кэше. Если в документ внесли изменения, ответ возвращается с кодом 200 и в хранилище браузера пишется новый ответ.
Чуть позже мы изменили метод кэширования и проверяли хеш-сумму в названии файла. Она гарантирует, что ресурс будет сохранять уникальность. Поэтому мы могли агрессивно кэшировать контент. В ответ добавлялся такой заголовок:
Cache-control:max-age=31536000, immutable
Max-age указывает максимальное время кеширование, в нашем случае оно равно 1 году. Immutable значение говорит о том, что такой ответ не нуждается в проверке на изменения.
Полезные ссылки
Это не все способы оптимизировать работу сайта. Сюда можно было добавить отказ от bootstrap, уменьшение количества DOM-элементов и ивентов и многое другое. Но это те советы, которые помогли нам сделать сайт быстрым и отзывчивым, несмотря на большое количество анимации.
Приглашаем в нашу команду
Мы всегда ищем крутых специалистов в свой офис в Санкт-Петербурге под наши амбициозные задачи: разработчиков, тестировщиков, дизайнеров. Ниже есть вакансии — присоединяйтесь к нам.
Автор: m_ePayments