Офер за 1 день в команду BetterMe (Frontend Hiring, JavaScript/React/Redux)
×Закрыть

Анимации, Google RAIL, что делать?

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

Анимация: создание кадра за 10мс

Анимация-это не просто необычные эффекты Ul. Например, прокрутки и касания — это типы анимации.

Пользователи замечают, когда в анимации частота кадров варьируется. Ваша цель — произвести 60 кадров в секунду, и каждый кадр должен пройти через все эти шаги:

Шаги, воспроизведения кадра

С чисто математической точки зрения, каждый кадр имеет бюджет около 16 мс (1000 мс / 60 кадров в секунду = 16.66 мс на кадр). Однако, потому что браузеру нужно немного времени, чтобы нарисовать новый кадр на экране, ваш код должен закончить выполнение за 10мс.

В точках высокой нагрузки, таких как в анимации, самое главное — это ничего не делать, когда это возможно, и делать по минимуму, когда невозможно. Когда это возможно, — использовать 100мс, предварительно просчитав объем работ так, чтобы был шанс попадания 60 кадров в секунду.

Дополнительные сведения см. в разделе «Отображение»

Цитата из Google RAIL, рекомендаций по скорости работы отзывчивых веб-приложений.

Предисловие

Анимации очень распространенный прием, с которым сталкиваются JavaScript-разработчики, Google не просто так включил их отдельным пунктом в RAIL, ведь именно анимации одни из самых прожорливых операций. Так как в своей самой распространенной JavaScript реализации меняют css-стили, многократно дергая DOM, что очень плохо сказывается на производительности такого скрипта.

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

Хорошо видна дерганность анимации на примерах jscrollpane, прокрутите скролл колесиком, тут FPS очень низок для того, чтобы сохранить производительность. А вот демки Custom Scrollbar — уже выглядит неплохо. Дерганье заметить практически невозможно, но тем не менее и у него есть возможность снизить частоту кадров, ведь даже самый быстрый скрипт станет медленным, если дергать его слишком часто. Кстати на нашем старом hp в офисе, кастом скроллбар уже заметно подергивается.

Решение Яндекса

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

Что делать?

el.animate()

Забудьте, эта штука еще крайне не кроссбраузерна, «Safari no support» не то, с чем согласится заказчик, а полифилы дадут такой же animate, как в jQuery.

Решение на поверхности

Да, но увы на чистом JavaScript решения пока нет, ведь без нативных анимаций вы так или иначе вы будете обращаться к el.style очень часто, потому могу посоветовать комбинировать javascript и css, такое решение будет элегантным, не зря ведь опытные разработчики постоянно говорят новичкам — «не пишите html/css в скриптах», очень не зря. Давайте рассмотрим, как с этим работать.

Для начала, банально переключим класс

const updateTransition = function() {
    return new Promise( (res, rej) => {
        var el = document.querySelector(".parent div");
        var isStatus = el.matches( '.box');
        el.classList.toggle( "box1", isStatus);
        el.classList.toggle( "box", !isStatus)
        
        var cb = function(){
            //once event
            el.removeEventListener("transitionend",cb, false);
            res();
            el.addEventListener("transitionend", cb, false);
        });
    }
var btn = document.querySelector("#btn");
    btn.addEventListener("click", () => {
        updateTransition().then( () => { console.log( `end ${Date.now()}`); }); 
    });

demo

Wow! много буков? давайте проще, в основе тут вот эти строки

//Находим элемент

var el = document.querySelector(".parent div");

//проверяем статус анимации

var isStatus = el.matches( '.box')

//ставим нужные классы анимации

el.classList.toggle( "box1", isStatus);

el.classList.toggle( "box", !isStatus);

Остальное просто обвязка для асинхронной работы и запуску по клику.

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

Что если заранее не известно есть ли анимация?

проверьте наличие свойства transition и animate после назначения класса.

А если отменят анимацию?

На самом деле это можно отследить только отслеживая изменения свойств, по крайней мере мне не известен иной способ. События transitioncancel увы нет, но намного чаще нужна функция просто остановки анимации, в том же jQuery это .stop(), однако при смене класса анимации, текущая анимация, автоматически остановиться и начнеться новая.

Цепочки анимаций?

Легко сделать используя цепочки промисов

    var chain = Promise.resolve();
    for( условие цикла ){
        chain = chain.then( ()=> {
            //код который надо чейнить
        })
    }

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

На последок, Много кода?

На самом деле только только el.classList.toggle( className), остальное — логика которая так или иначе у вас будет присутствовать в приложении.

Послесловие с благодарностью нашим студентам

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

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

Код в статье взят из домашек студентов ProCode.com.ua, Николая Кравченко и Максима Бурды, промисы детально рассматриваются у нас на курсе «JS Frontend», цепочка анимаций на промисах один из стандартных применений этого паттерна.

👍НравитсяПонравилось0
В избранноеВ избранном0
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Ваша демка — ужасно лагает. Изучайте основы, какие свойства в CSS можно трогать для плавной анимации / какие нет, что происходит когда мы меняем класс элемента и происходит(или не происходит) его рендер и вынесение на отдельный слой, обращение к каким свойствам в JS может вызвать ненужные пересчеты. В гугл доках многое из этого описано.

Если надо отслеживать и очень аккуратно обрабатывать события, то лучше подключить библиотеку с Observable типа RxJS и наслаждаться жизнью с различными debounce(). Что касается цепочек анимации — promise там только лишний костыль.

Качественный код JS-анимации — это когда не используются несколько циклов requestAnimationFrame, а все просиходит в одном. Для этого во всех вменяемых animation/motion библиотеках существует объект Timeline в который и добавляются наши анимации в произвольном порядке и c произвольной длительностью, и мы получаем над ними полный контроль. Что касается изменения свойств и анимации через CSS — мы при запуске анимации знаем ее длительность и достаточно использования setTimeout т.к. он более гибок. Запуск анимации после того как другая закончилась — редкий и простой кейс. Если нужно запустить другую анимацию за 200мс до окончания прошлой, а такое бывает чаще, мы все равно привязаны к значениям длины конкретной анимации.

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

Покажите как вы ее используете, не должна лагать.

Качественный код JS-анимации
Ну про это во второй части будет, это не качественный а просто случай когда анимация часть логики. Что касается setTimeout, не уверен о чем вы, использовать так как говорите в лоб нельзя, у как будут скакать анимации +/- по времени, выглядит ужасно.

Ок, с вами как с политиком =). Невозможно разбудить человека, который притворяется спящим.

а просто случай когда анимация часть логики
Хорошо, допустим. Посмотрим на вашу логику, т.к. дальше я тут комментировать не буду. Во-первых в коде ошибка:
var cb = function(){
  el.removeEventListener("transitionend",cb, false);
  res();
  el.addEventListener("transitionend", cb, false);
});
Привязку надо вынести за блок, иначе она не сработает. Во-вторых вы на каждый клик не обрабатываете изменение события, а просто создаете новое. В итоге, если 5 раз клацнуть кнопку до того, как блок пройдет всю дистанцию — ваш колбэк запуститься в конце 5 раз. И если у вас «цепочка», то ваша следующая анимация стартанет 5 раз. Это недопустимое поведение, которое может привести к загадочным багам. И то что каждый раз при запуске вы обращаетесь к тяжелому DOM, вместо того, чтобы 1 раз сделать querySelector и дальше им пользоваться — тоже косяк логики.

Еще раз повторяю. Ваш пример к анимации и, тем более, цепочкам анимации — не имеет никакого отношения. Пример абсолютно надуманный.
Привязку надо вынести за блок, иначе она не сработает.
Очень даже сработает, это типовой паттерн.
. В итоге, если 5 раз клацнуть кнопку до того, как.....
Не будет, сработает третий параметр на тугле, но в целом понятно о чем вы, можно и так как вы описываете, это не имеет прямого отношения к самой анимации, в данном случае идет демонстрация за что можно зацепиться, а как, уже дело конкретного случая.
И то что каждый раз при запуске вы обращаетесь к тяжелому DOM
Оно и сохранено в переменную el и querySelector далее не фигурирует, третья строка.

Вы даже очевидные вещи отрицаете.

Очень даже сработает, это типовой паттерн.

Смотрите, по вашей демке, меняем код как вы выше написали (изначально там правильно):
i.piccy.info/...58467/72485/1119686/1.png
Клац-клац — в консоли ничего нет.

Не будет, сработает третий параметр на тугле
Не знаю, что такое «тугле» в вашем понимании, но если повесить 5 обработчиков через EventListener, то они все сработают, пример с вашей демки:
i.piccy.info/...58714/79923/1119686/2.png
Вот ваши, отработанные 5 раз подряд, console.log-и после событий transitionend.
Оно и сохранено в переменную el
Оно его сохраняет в локальную переменную функции каждый раз, когда она запускается. В таких случаях пишется ф-я высшего порядка, которая возвращает нужную нам ф-ю с замыканием на 1 раз подхваченный элемент, или просто элемент выноситься в глобальные переменные.
данном случае идет демонстрация за что можно зацепиться
В данном случае идет демонстрация чего-угодно, что никак не связано с анимацией, ее управлением, RAIL, 60fps и всего остального. Если вы этого не понимаете — вашим студентам стоит задуматься.
Клац-клац — в консоли ничего нет. (и прочие)

Понял о чем вы, код выше очевидно же что опечатка, у DOU нет никакого редактора когда пишешь статью, в демке код написан верно, спасибо что заметили. О паттерне имелось ввиду var cb = function(){ ..... cb()}
Не знаю, что такое «тугле»
toogle имелся ввиду el.classList.toggle( «box1», isStatus); параметр isStatus отвечает за то чтобы только добавлять или только отнимать
Вот ваши, отработанные 5 раз подряд, console.log-и после событий transitionend.
Так добавьте чтобы не было, проблема в чем? детская задача как вы заметили.
....локальную переменную функции каждый раз, когда она запускается. В таких случаях пишется ф-я высшего порядка, которая....
Только тогда когда это нужно для логики приложения, не меньше ситуаций когда этого ненужно.

60fps на анимацию — это понты и не более. Аниме, которое мультики, анимировано 15 кадрами в секунду чуть менее чем всё. И при этом рубит кассу не хуже Голливуда. А вы тут рассказываете за 60fps в браузере.

Вместо того чтобы онанировать на монополиста, давая ему ещё больше монополии — разобрались бы лучше чего люди хотят на самом деле. Принцип грамотного UI не менялся уже десятки лет: не заставляйте меня думать!

ЗЫ. Попокорн не анимирован, почему люди его до сих пор употребляют? Это неправильный попкорн, даёшь с анимацией!

60fps на анимацию — это понты и не более. Аниме, которое мультики, анимировано 15 кадрами в секунду чуть менее чем всё.
Поддерживая по духу заявления хочу таки классическую матчасть напомнить, типа что фильмам 24 фпс хватает, а играм нет, т.к. у фильмов motion-blur присутствует и в аниме его иногда рисуют, а в последнее время может и рендерят. А чем рендерить реалтайм блюр может быть лучше фпс поднять. И ещё момент, что неравномерность фпс может хреново смотреться.

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

Игры — отдельная тема. Но когда каждый наскоро состряпанный банер начнёт рендерить свои 60fps — внезапно окажется что одна вкладка может выжрать CPU на 100%.

Для того чтобы поднимать fps нужна модель фреймбуфера, а не грёбаная DOM-модель.

Що-о ? А як же сніжинки ? Падаючі сніжинки ! Який же сайт без сніжинок ?!

Подписаться на комментарии