Что здесь не так?


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


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


Вкладка Rendering в консоли Chrome помогла подтвердить факт перерисовки:





Мы видим перерисовку каждую секунду и средний fps 50 кадров и это на странице, где у нас изображены всего одни часы.


А как еще можно?


И так я поставил перед собой задачу создать аналоговые часы, который не будут вызывать перерисовку страницы и полагаться на JavaScript для вычисления положения стрелки. Часы которые не будут никак влиять на частоту кадров в браузере. Среди всех мне известных CSS свойств, только одно позволяет трансформировать элемент без перерисовки — transform. Значит его и будем использовать.

Но сначала создадим свои часы, с которыми нам будем удобно шевелить стрелку одним только transform свойством. Создадим циферблат со всеми стрелками:



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

.clock__hand {
    margin-left: -0.5em;
    margin-top: -0.5em;
    font-size: inherit;
    position: absolute;
    display: block;
    height: 1em;
    width: 1em;
    left: 50%;
    top: 50%;
}
.clock__hand--hour::after {
    content: "";
    border-radius: 0.015em 0.015em 0.01em 0.01em;
    background-color: #000;
    margin-bottom: -0.02em;
    margin-left: -0.025em;
    font-size: inherit;
    position: absolute;
    display: block;
    height: 0.25em;
    width: 0.05em;
    bottom: 50%;
    left: 50%;
}

С помощью font-size в данном случае мы задаем размер циферблата и всех компонентов часов.

Код вида самых часов.


Хорошо, а на сколько вращать?


Вращать стрелку на все 360 градус. Правильный вопрос: как долго ее вращать? А зависит это от того, какую стрелку мы вращаем. Часовую — 12 часов, минутную — час, секундную — минуту.

.clock__hand--hour {
    animation: clock-hand-rotate 43200s linear infinite;
}
.clock__hand--minute {
    animation: clock-hand-rotate 3600s linear infinite;
}
.clock__hand--second {
    animation: clock-hand-rotate 60s linear infinite;
}
@keyframes clock-hand-rotate {
    from {
        transform: rotate(0deg)
    }
    to {
        transform: rotate(360deg)
    }
}

И так наши часы заработали.

А что же нам скажет про них Chrome?



Никаких перерисовок и стабильных 60 fps


Но это же не наше время.


И так наше начальное время 00:00:00 потому что все стрелки начинают анимацию с 0 градусов. Чтобы начинать с настоящего времени нам нужно рассчитывать начальный градус отдельно для каждой стрелки относительно времени. И так у нас два варианта, либо на стороне сервера рендерить CSS относительно времени запроса, либо использовать JavaScript. Конечно сервер рендеринг и без скриптовые часы это круто, но ради подтверждения концепта мы все же используем JavaScript.

var date = new Date(),
    hours = date.getHours(),
    minutes = date.getMinutes(),
    seconds = date.getSeconds();

if (hours > 12) {
    hours -= 12;
}

var secondsStartDegree = 360 / 60 * seconds,
    minutesStartDegree = 360 / 60 * minutes + 6 / 60 * seconds,
    hoursStartDegree = 360 / 12 * hours + 30 / 60 * minutes + 0.5 / 60 * seconds;

var style = document.createElement('style');

style.type = 'text/css';
style.innerHTML = '        @keyframes clock-hand-rotate--hour {            from {transform: rotate(' + hoursStartDegree + 'deg)}            to {transform: rotate(' + (hoursStartDegree + 360) + 'deg)}        }        @keyframes clock-hand-rotate--minute {            from {transform: rotate(' + minutesStartDegree + 'deg)}            to {transform: rotate(' + (minutesStartDegree + 360) + 'deg)}        }        @keyframes clock-hand-rotate--second {            from {transform: rotate(' + secondsStartDegree + 'deg)}            to {transform: rotate(' + (secondsStartDegree + 360) + 'deg)}        }        .clock__hand--hour {            animation: clock-hand-rotate--hour 43200s linear infinite;        }        .clock__hand--minute {            animation: clock-hand-rotate--minute 3600s linear infinite;        }        .clock__hand--second {            animation: clock-hand-rotate--second 60s steps(60) infinite;        }';

document.getElementsByTagName('head')[0].appendChild(style);

И там мы создаем 3 анимации для каждой из стрелок и подключаем их к соответственным классам.

Вот результат.

А что еще можно?


Ну если вам не хватает того факта, что у вас часы которые почти не забирают ресурсов браузера. То вот еще пару фишек:


Хорошо а в чем минусы


Их не так уж много, но они есть:

  • Так как часы отсчитывают время CSS-ом, то если забить тред какими-то тяжёлыми заданиями — часы перестанут идти и когда страница освободится — будут отставать. Но все это можно поправить заново задав keyframe.
  • Не всюду сайт будет выглядеть идеально, вот например в том же IE11 некоторые центры немного смещены:


Итоги


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

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


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


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

Комментарии (16)


  1. domix32
    11.03.2018 21:19

    CSS и ничего больше

     


    document.getElementsByTagName('head')[0].appendChild(style);

    Это что-то вроде ничего_больше.js?


    1. StasL Автор
      11.03.2018 22:58

      JavaScript был использован только для инициализации часов, создания начальных стилей. Сами часы идут только на основе CSS анимации.
      Но если вас интересует только CSS+HTML часы — можно перенести создание CSS анимации на серверную часть, к примеру можно сгенерировать те же стили при помощи PHP, тогда на стороне клиента все будет работать даже если отключены скрипты.
      Я кстати об этом упоминал в статье:

      И так у нас два варианта, либо на стороне сервера рендерить CSS относительно времени запроса, либо использовать JavaScript. Конечно сервер рендеринг и без скриптовые часы это круто, но ради подтверждения концепта мы все же используем JavaScript.

      Простите, что не уточнил сразу в статье.


      1. domix32
        12.03.2018 01:33

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


        1. StasL Автор
          12.03.2018 13:56

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

          На счет нагрузок уже отписывал, но все же
          Я не знаю как себя поведет CSS при нагрузках процессора, я поэксперементирую с этим и сообщу вам результат. Но, хорошо сделанная страница не должна нагружать поток на столько, чтобы часы сильно отставали (больше 5 секунд разницы в час), а такими малыми отклонениями можно пренебречь.
          Также, ради эксперимента оставлял вкладку с часами открытой, пока было запущено пару тяжелых процессов и еще 20 вкладок и часы за 2 часа так и не отстали.
          В случае сна и гибернации — ничего не гарантирую. Но можно переопределять CSS классы при возвращении пользователя на страницу, много проблем это не вызовет.

          Сама статья — это все лишь концепт, а не готовое решение.


  1. Leonso
    11.03.2018 22:47

    Интересно, спасибо большое.


  1. your_eyes_lie
    11.03.2018 22:47

    Ещё забавная реализация часов без клиентского (только gif) рендеринга:
    bolknote.ru/all/3300


  1. Co0l3r
    12.03.2018 01:42

    средний fps 50 кадров

    Если часы обновляются раз в секунду, откуда вообще может взяться 50 кадров в секунду?
    Чтобы измерить быстроту отрисовки, нужно поставить постоянную перерисовку часов, например вот так:


    // в setTimeVariables
    setCustomProperty('seconds', time.getSeconds() + time.getMilliseconds() / 1000);
    
    const start = () => {
        setTime()
        requestAnimationFrame(start)
    }
    
    start()

    Тогда можно увидеть стабильные 60фпс (или сколько частота обновления на мониторе)
    Также, если поставить стрелкам will-change: transform, то они не будет репайнта на каждый кадр.
    Так что css-переменные тут вообще не причем.


    1. Serator
      12.03.2018 10:30

      Поддерживаю. В статье изложены неверные выводы. Переменные вообще никак к отрисовке не относятся и сами по себе ее не вызывают. Просто в одном варианте часов отдельный слой создается при каждом сдвиге стрелки, а во втором варианте слой существует постоянно.
      Для плавности же можно просто добавить тех же стилей вроде `transition: 1s linear;`. В итоге код в статье сильно усложнился / запутался, а прибавилось только минусов. %)


      1. StasL Автор
        12.03.2018 13:32

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

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


        1. Serator
          14.03.2018 19:33
          +1

          Ваше утверждение немного неверно. 60fps — этакий стандарт для браузера. Соответственно, 60 вызовов функции перерисовки внутри браузера. Но суть не в этом. Я пока отброшу второй абзац вашего ответа, так как, если основываться на нем, то переменных (пользовательских свойств) CSS вообще быть не может, так как нет поддержки. В оригинальной статье автор обновлял DOM не только с целью проброски CSS переменных, но и с целью обновления данных для доступности, чего у вас уже нет. Для глаз это погоды не делает, а вот для слуха очень даже (по сему поводу чистая теория, так как нет желания проверить). Идем дальше. Ваш код создает отдельный слой и крутит его. И этот слой висит постоянно из-за анимации и не объединяется с другими слоями (http://prntscr.com/ir5mc8), посему вы не видите перерисовки. Такая «магия» дается не бесплатно, на нее выделяется память, что, как бы, тоже ресурс системы. Выше вам уже писали про «волшебное» свойство `will-change`, которое может сделать тоже самое для решения из оригинальной статьи. Добавьте `transition` и увидите теже 60FPS (появится постоянная перерисовка). Ну а если учесть

          Так как часы отсчитывают время CSS-ом, то если забить тред какими-то тяжёлыми заданиями — часы перестанут идти и когда страница освободится — будут отставать. Но все это можно поправить заново задав keyframe.
          то получается, что нам нужно точно так же пробрасывать в DOM кучу стилей не реже, чем раз в секунду. Так в чем профит-то? IE11?

          И про загадочные 50.2FPS все тоже просто и понятно. Если открыть счетчик FPS на оригинальных часах, то там будет 1FPS (логично же, 1 перерисовка всего, но, на самом-то деле, это тоже может варьироваться, так как процессор можно нагрузить так, что будет 0FPS). На вашем же скрине видно, что включена настройка отображения перерисовок (зеленый прямоугольник), которая сама по себе вызывает перерисовку, так как этот самый зеленый прямоугольник появляется / исчезает плавно. Вот и FPS проседает (а проседает ли? :)).


          1. StasL Автор
            14.03.2018 20:01

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

            Вы очень не правы тут:

            Соответственно, 60 вызовов функции перерисовки внутри браузера.

            И даже тут:
            Так в чем профит-то? IE11?
            Каждый браузер важен на столько, на сколько им пользуются именно ваши посетители ресурса.

            И я был не прав тут:
            Так как часы отсчитывают время CSS-ом, то если забить тред какими-то тяжёлыми заданиями — часы перестанут идти и когда страница освободится — будут отставать.
            Хотя зависит от реализации keyframe в браузере. Но многим такие вещи не по чем.

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


    1. StasL Автор
      12.03.2018 13:24

      Здравствуйте, рекомендуймый FPS для страницы, не зависимо от контента — 60. И к сожалению, так с ходу, я не могу вам сказать почему в данном случае он 50, но факт остается факто — мы пишем в DOM с помощью JS каждую секунду и это нагружает главный поток. Кроме того наше переопределение CSS переменных вызывает перерасчет некоторых CSS аттрибутов, что нагружает главный поток опять.
      Интереса ради я просто попробовал изменять CSS переменные каждую секунду в пустой странице, и даже так у меня появлялись странный просадки FPS, хотя не так частые. Я не утверждаю что не стоит вообще не использовать CSS переменные, просто возможно не стоит только ради часов нагружать главный поток.

      На счет вашей функции сверху — она покажет FPS на основе только одного кадра, что не говорит ни о чем. Чтобы узнать точное FPS нужно считать среднее значение хотя бы 100 кадров. А так, это как говорить, что все люди выше 2 метров на основании роста Шакила О'Нила. Потому, простите, по Хром тут точно лучше обчистил FPS.


  1. Turba
    12.03.2018 06:32

    Да, это вполне очевидное решение, тоже вначале подумал об этом.
    Но, смутило как-раз то, о чём вы упомянули. Лаги могут появиться не только от загруженности страницы, но и от загрузки проца.
    И ещё мне не понравилось то, что атрибут datetime не будет меняться. Т.е. не факт, конечно, что это нужно. Но могут быть случаи, когда текстовая составляющая также важна, как и визуальная. Я имею в виду скринридеры.
    Если это не столь важно, то, конечно, такой способ приоритетней.
    Спасибо.


    1. StasL Автор
      12.03.2018 13:49

      Я очень сомневаюсь что хоть одна читалка будет рада ежесекундному изменению времени. Я работал пару раз с читалками и для них достаточно важно не изменять контент без надобности. Да и представте себе — читалка еще не успела прочитать время, а вы уже его поменяли 5 раз.
      Я бы сказал, что для читалок лучше спрятать такой элемент (aria-hidden=«true») и создать скрытый элемент где мы меняем время раз в минуту.

      На счет нагрузок — да. Я не знаю как себя поведет CSS при нагрузках процессора, я поэксперементирую с этим и сообщу вам результат. Но, хорошо сделанная страница не должна нагружать поток на столько, чтобы часы сильно отставали (больше 5 секунд разницы в час), а такими малыми отклонениями можно пренебречь.
      Также, ради эксперимента оставлял вкладку с часами открытой, пока было запущено пару тяжелых процессов и еще 20 вкладок и часы за 2 часа так и не отстали.
      В случае сна и гибернации — ничего не гарантирую. Но можно переопределять CSS классы при возвращении пользователя на страницу, много проблем это не вызовет.

      Сама статья — это все лишь концепт, а не готовое решение.


  1. the_volt
    12.03.2018 15:43

    Очень интересная статья, спасибо автору. Была подобная идея, но до реализации руки не дошли.


  1. ivan386
    13.03.2018 00:02

    Очень странно что не смотря на то что картинка очевидно меняется хром и firefox не подсвечивают перерисованную область. Но график fps в хроме при этом активно двигается показывая постоянную перерисовку.


    В недавней статье "Этот SVG всегда показывает сегодняшнюю дату" в комментариях разместили часы в SVG. Они работают как секундомер на странице.


    Анимация в SVG работает даже если вставить его в тег IMG. Но тогда начинает перерисовываться полностью всё изображение. И этот секундомер не хило нагружает процессор.


    Я поэкспериментировал и сделал свой SVG секундомер в котором анимация включается каждую секунду на 0.001 секунды.
    image


    Думаю fps при этом маленький не потому что нагрузка большая а потому что рисовать нечего. Каждую секунду рисуется новый пик и график останавливается до следующего шага(если не включена подсветка прорисованной области).


    Если включить подсветку прорисованной области то можно увидеть что картинка обновляется раз в секунду.