Big image in HTML

В статье речь пойдет о том, как подключать в web страницу объемные элементы анимации, и не поломать все и сразу.


Если вы очень переживаете за показатели Google Page Speed в разработке сайтов, и у вас подгорает за каждый лишний килобайт не стоит продолжать читать данную статью.


Тех же, кого не пугают большие размеры, и любит риск прошу под кат ;)


Задача бизнеса


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


Попытки, которые надо было сделать, что бы прийти к оптимальному варианту:


  1. Самый банальный способ — создаем ссылку из gif файла. У формата есть поддержка анимации, альфа канала и прочее… чего ещё надо?!


    Вариант не подошёл, оказывается, если на картинке много прозрачностей и градиентов, то это сильно портит качество, (gif это всего 256 цветов), ну и размер файла получается 19мБ, что плохо.

  2. Пошёл искать аналог, с хорошей поддержкой. В процессе серфинга попался APNG. Формат из себя представляет png с поддержкой анимации, по кроссбраузерности всё кроме IE11 у него хорошо. Готовый файл стал весить поменьше, (17Мб) но результат оказался всё ещё не удовлетворительный. В процессе анимации наблюдается пиксилизация изображения. Получше чем у gif, но всё ещё видно.

  3. Все прогрессивные люди советуют анимацию хранить в видео форматах, чем мы хуже !?
    К сожалению, 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> 
    

  4. Что ж перейдем к выбранному варианту, который удовлетворил заявленным условиям задачи. А именно использование свойства:
    animation-timing-function: steps(…)
    Замечательное свойство, к сожалению, редко встречается на практике.;

Первое что необходимо иметь это — секвенция кадров (от англ. «sequence» (последовательность, ряд)), или по-простому – раскадровку. Находим в загашниках аниматоров, или если повезло, просим сделать последовательную раскадровку понравившейся анимации.


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




После этого, нам необходимо собрать всё это дело в спрайт(Один большой совмещенный файл).


Тут кому как удобнее – можете воспользоваться, скажем, плагином для gulp сборщика

gulp.spritesmith

Или же пойти моим путем, и воспользоваться уже готовым приложением компании Toptal, при разработке Chris Coyier генератором спрайтов.


Тут всё предельно просто. В поле padding-ов ставим 0, в поле “Choose files” перетаскиваем наши кадры.

! Важно чтобы элементы были правильно пронумерованы или названы в правильной последовательности;
! Все файлы должны быть одинаковых масштабных размеров.

Получаем готовый результат, который скачиваем.


Css sprites generator

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


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


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


*Спойлер
Когда выкатил в продакшен вариант, описанный выше, показатель Performance от Google Lighthouse рухнул от 80 до 20 за пару часов.
Так что пока у нас нет общедоступных 5G сетей, а Илон Маск продолжает запускать спутники Neuralink прийдеться что-то выдумывать что бы достигать приемлемых результатов.

Процесс оптимизации слона


Первое что пришлось сделать, это поумерить свою хотелку и пересобрать анимацию с 91кадра до 73кадров. Спрайт остался всё ещё тяжелым (5,17Мб), но с ним, оказывается, может теперь работать Squoosh (Там стоит какое-то ограничение по ширине загружаемого файла <16300px)


Это привело к дилемме, рационально ли пожертвовать пользователями IE и людьми, которые пользуются Safari, но сделать хорошо для всех остальных?
Ответ очевиден. Переводим спрайт в формат webP
(Размер спрайта уменьшился до 2,47Мб Squoosh предлагал ужать и больше, до 1,67Мб, но там уже посыпались артефакты, а нам это не надо);


*Тут внимательный читатель мог бы предложить определять window.navigator.userAgent, а потом отдавать png спрайт как альтернативу обладателям уникальных браузеров, что бы все видели анимацию, но идея не получила одобрения в силу здравого смысла.


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


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



Чтобы избежать данного эффекта, ставим блоку обертке на начальном этапе значение display:none; и только когда изображение загрузилось, показываем его пользователю. Код выглядит приблизительно так.


HTML
<div class="wrapper">
  <div class="myAnimation"></div>
  <p>Я загрузился!</p>
</div>

CSS
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;
}

JS
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 по ссылке.


logo NetGame Entertainment

Атрибуции


Благодарю руководство компании Netgame Entertainment за возможность использования в своей статье материалов компании, а также за предоставление раскадровки персонажа анимации Максимом Черноусом.

Также благодарю OPTIMUS PRIME за подсказку с кодом в оптимизации.



Сделаем web лучше.