В статье речь пойдет о том, как подключать в web страницу объемные элементы анимации, и не поломать все и сразу.
Если вы очень переживаете за показатели Google Page Speed в разработке сайтов, и у вас подгорает за каждый лишний килобайт не стоит продолжать читать данную статью.
Тех же, кого не пугают большие размеры, и любит риск прошу под кат ;)
Задача бизнеса
Для начала опишу задачу, которая была поставлена бизнесом.
Необходима якорная ссылка с анимацией, которая будет зафиксирована в окне браузера. Выглядеть должно хорошо.
Попытки, которые надо было сделать, что бы прийти к оптимальному варианту:
Самый банальный способ — создаем ссылку из gif файла. У формата есть поддержка анимации, альфа канала и прочее… чего ещё надо?!
Вариант не подошёл, оказывается, если на картинке много прозрачностей и градиентов, то это сильно портит качество, (gif это всего 256 цветов), ну и размер файла получается 19мБ, что плохо.
Пошёл искать аналог, с хорошей поддержкой. В процессе серфинга попался APNG. Формат из себя представляет png с поддержкой анимации, по кроссбраузерности всё кроме IE11 у него хорошо. Готовый файл стал весить поменьше, (17Мб) но результат оказался всё ещё не удовлетворительный. В процессе анимации наблюдается пиксилизация изображения. Получше чем у gif, но всё ещё видно.
- Все прогрессивные люди советуют анимацию хранить в видео форматах, чем мы хуже !?
К сожалению, mp4 как общепринятый стандарт мне не подходил по условию задачи. Поскольку якорь, должен скролится, то было бы неплохо, если б фон не зависел от текущего положения анимации в документе.
С mp4 такого нельзя, фон запекается вместе с изображением, если он не назначен дизайнером, будет белым.
В моём случаи страница сайта сделана секционной, и это будет бросается в глаза при прокрутке.
Хотя если вы планируете тоже в своих проектах использовать зацикленные анимации, то я бы вам рекомендовал подключать их именно через тег video.
Особенно если у вас однотонный фон.
<video class="bl_video" loop="" muted="" autoplay="" poster="poster.jpg"> <source src="topbanner.webm" type="video/webm"> <source src="topbanner.mp4" type="video/mp4; codecs="avc1.42E01E, mp4a.40.2""> </video>
- Что ж перейдем к выбранному варианту, который удовлетворил заявленным условиям задачи. А именно использование свойства:
animation-timing-function: steps(…)
Замечательное свойство, к сожалению, редко встречается на практике.;
Первое что необходимо иметь это — секвенция кадров (от англ. «sequence» (последовательность, ряд)), или по-простому – раскадровку. Находим в загашниках аниматоров, или если повезло, просим сделать последовательную раскадровку понравившейся анимации.
Анимация должна смотреться плавно, не дергано, и последний кадр должен визуально быть сопоставимым с первым.
После этого, нам необходимо собрать всё это дело в спрайт(Один большой совмещенный файл).
Тут кому как удобнее – можете воспользоваться, скажем, плагином для gulp сборщика
gulp.spritesmithИли же пойти моим путем, и воспользоваться уже готовым приложением компании Toptal, при разработке Chris Coyier генератором спрайтов.
Тут всё предельно просто. В поле padding-ов ставим 0, в поле “Choose files” перетаскиваем наши кадры.
! Важно чтобы элементы были правильно пронумерованы или названы в правильной последовательности;! Все файлы должны быть одинаковых масштабных размеров.
Получаем готовый результат, который скачиваем.
В результате эксперимента было выяснено, что горизонтальный спрайт занимает меньше размер, чем вертикальный составленный из тех же кадров, хотя для меня и осталось это загадкой. (Если кто-то знает ответ, почему такой метод сжатия лучше, напишите в комментариях).
Сразу оговорюсь, тут есть жесткие ограничения по размеру получаемого.
Если ваш спрайт весит > 5Мб, вы автоматом получаете дополнительные проблемы.
Мой сформированный png спрайт получился огромного размера. (7,92Мб)
К сожалению, ни один из онлайн минификаторов изображения не согласился работать с картинкой такого размера. (Чаще всего использую TinyPNG или Squoosh)
Больше того, даже плагин gulp-imagemin поломался с ошибкой –
«Слишком большой размер обрабатываемого файла, умерь свои запросы.»
Ну что ж не беда, значит, сожмем кадры первоисточника по очереди, а потом сделаем спрайт.
Меня терзало сомнение, а не испортиться ли качество анимации после сжатия, но обошлось. Наверное, это было связано с тем, что при частоте 25-40мс на кадр глаз не успевает заметить серьезные дефекты.
После компрессии файл стал весить на 1.2мБ меньше, что уже хорошо.
Итак, мы получили, спрайт, теперь с ним надо как-то работать. Пишем CSS для нашей анимации, только под десктопную версию браузера. Пожалеем пользователей с дорогим мобильным трафиком.
.toContactForm_leprechaun {
width: 220px;
height: 290px;
box-sizing: border-box;
background: url(../image/leprechaun-sprite.png) 0 50%;
animation: play 3276ms steps(91) infinite;
}
@keyframes play {
to {
background-position: -20020px;
}
}
Надо сделать описание, что б всем было понятно, в чём тут магия.
1) Параметрами width и height задаем область видимости одного кадра (напомню, что все кадры у нас имели одинаковые width и height).
2) Задаем в фоновое изображение — адрес расположения спрайта позиционированного слева по оси Х и отцентрированного относительно Y.
3) Пишем вызов анимации. Анимация у нас линейная, бесконечно повторяемая.
Длительность анимации равна коррекционное время * количество кадров.
Значение steps равно количеству кадров.
Коррекционное время подбирается по собственным ощущениям и количеству кадров в наличии, в моем случаи, это в пределах 30мс – 45мс на кадр.
4) Для ключевого описания анимации достаточно прописать только конечное положение анимации.
Считается оно из следующей формулы = (-1px)*количество кадров*width каждого кадра.
*Если вы допустите неточность в данном подсчете, то вы очень быстро увидите свою ошибку. Тут всё должно быть до пикселя.
Собственно анимация готова. Можно продолжать радоваться жизни.
Половина дела сделана, можно идти пить чай. Если у вас простенькая анимация, и размер файла не превышаем 100кБ, не стоит заморачиваться.
Добавляйте ваш якорь как можно ближе к закрывающему тегу body или подгружайте лениво, после того как отрендерился DOM и отработали все нужные скрипты, этого будет достаточно.
Если же вам выпал хардкорный вариант, где разговор уже про мегабайты тут следует подумать.
Так что пока у нас нет общедоступных 5G сетей, а Илон Маск продолжает запускать спутники Neuralink прийдеться что-то выдумывать что бы достигать приемлемых результатов.
Процесс оптимизации слона
Первое что пришлось сделать, это поумерить свою хотелку и пересобрать анимацию с 91кадра до 73кадров. Спрайт остался всё ещё тяжелым (5,17Мб), но с ним, оказывается, может теперь работать Squoosh (Там стоит какое-то ограничение по ширине загружаемого файла <16300px)
Это привело к дилемме, рационально ли пожертвовать пользователями IE и людьми, которые пользуются Safari, но сделать хорошо для всех остальных?
Ответ очевиден. Переводим спрайт в формат webP
(Размер спрайта уменьшился до 2,47Мб Squoosh предлагал ужать и больше, до 1,67Мб, но там уже посыпались артефакты, а нам это не надо);
*Тут внимательный читатель мог бы предложить определять window.navigator.userAgent, а потом отдавать png спрайт как альтернативу обладателям уникальных браузеров, что бы все видели анимацию, но идея не получила одобрения в силу здравого смысла.
Поскольку в моем случае так называемая якорная ссылка состоит из элемента анимации и кнопки, а как не крути это серьезные размеры для веба, следует ещё предусмотреть момент её отображение.
Разуметься, данный элемент несет декоративный характер, а значит он должен рендерится браузером в последнюю очередь. Поэтому размещаем его поближе к концу документа. Также не хочется видеть эффект прогрузки большого изображения.
Чтобы избежать данного эффекта, ставим блоку обертке на начальном этапе значение display:none; и только когда изображение загрузилось, показываем его пользователю. Код выглядит приблизительно так.
<div class="wrapper">
<div class="myAnimation"></div>
<p>Я загрузился!</p>
</div>
body {
margin: 0;
}
.wrapper {
display: none;
padding: 10px;
border: 1px solid #000;
}
.wrapper.active {
display: block;
}
.myAnimation {
display: inline-block;
width: 247px;
height: 187px;
background: url("https://gyazo.com/f5d013014306342a2241f8d3b8fb11ea.png") 50% 50% no-repeat;
}
p {
display: inline-block;
width: 150px;
vertical-align: top;
}
awaitBgImgLoad();
function awaitBgImgLoad() {
var div = document.querySelector('.myAnimation');
var src = window.getComputedStyle(div).backgroundImage;
console.log(src); // адрес хранится в виде `url("src")` — надо удалить лишние символы
src = src.replace(/url\(|\)|"/g,"")
loadAndRun(src, onload);
/***/
function onload() {
console.log("Я загрузился!");
document.querySelector('.wrapper').style.display = "block";
}
}
/*****/
function loadAndRun(src, resolve, reject) {
var img = new Image();
img.onload = resolve;
img.onerror = reject || function(){
console.log("Не загрузилась " + src)
};
img.src = src;
}
Если вам хочется что б ваша анимация загружалась пораньше (не знаю, зачем это может понадобиться), можно добавить в тег ‹head›
<link rel="preload" href=" bigAnimationImg.webP" as="image" />
И вместо генерации и отслеживания копии изображения средствами JS, можно разместить копию данного изображения со значениями стилей:
<img class="animation-image" width="220" height="290" src="bigAnimationImg.webP" loading="eager" alt="animation"/>
.animation-image{
position:absolute;
top:0;
left:calc(-100%+1px);
}
Где-то перед якорной ссылкой, повыше к основному контенту.
В процессе рендеринга страницы, браузер сначала “увидит” обращение к картинке и пойдет за ней по src, а уже потом из кеша будет подставлять это же изображение в свойство background-image для второго элемента. (Приоритет выдачи картинок для тега img обычно выше, чем для background-image)
* Автор понимает, что это очень экзотический метод обмана браузера, и не рекомендует его к использованию.
С готовой сборкой можно ознакомиться на сайте компании Netgame Entertainment по ссылке.
Атрибуции
Благодарю руководство компании Netgame Entertainment за возможность использования в своей статье материалов компании, а также за предоставление раскадровки персонажа анимации Максимом Черноусом.
Также благодарю OPTIMUS PRIME за подсказку с кодом в оптимизации.
Сделаем web лучше.
sfi0zy
Можно пойти по пути хромакея, «как в кино». В видео фон делаем однотонным, того цвета, который нигде в нем больше не используется. Затем из кадров видео на лету делаем текстуры, которые отображаем на одной плоскости-«экране» на канвасе в WebGL контексте. Дальше остается дело за малым — во фрагментном шейдере заменяем цвет хромакея на прозрачный и имеем видео с прозрачным фоном. Учитывая размер изображения в решаемой задаче, это не сильно повлияет на производительность.
vladkorotnev
От клуба пользователей интернета с мобильников и ноутбуков на батарейках передаём вашему клубу забивателей гвоздей микроскопом пламенные лучи поноса.
sfi0zy
Хорошими практиками при разработке рекламных сайтов с разными эффектами считается отключать большую часть анимаций (или даже все) на телефонах, а в вопросах производительности исходить из того, что у пользователя будет слабенький ноутбук. Так что с загрузкой или перегревом дела будут обстоять не хуже, чем у среднего SPA-магазина. Отдельные команды допускают у себя подход «и так сойдет, лишь бы работало», но это их личные проблемы, а не проблемы всего класса таких сайтов.
BlackStar1991 Автор
Спасибо, за интересный вариант, Иван. Обязательно опробую ваш метод.