company_banner

Аппаратное ускорение в жизни верстальщика. Семинар в Яндексе

    Привет! Меня зовут Александр Завьялов. В Яндексе я занимаюсь разработкой интерфейсов. Недавно я выступил перед коллегами с докладом об аппаратном ускорении в жизни верстальщика, где также коснулся смежных тем. Рассказал о производительности веб-страниц, о том, как она измеряется и к чему она может стремиться.



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

    Начну я с примера: одной из промо-страниц Яндекс.Браузера. На этом сайте мы видим слайдер на всю страницу. Сейчас ее уже переделали, но если посмотреть сохраненную копию, там не все так гладко. Заходим в таймлайн, включаем запись и начинаем прокликивать слайды. Видим, что все не очень хорошо — даже в 30 fps мы укладываемся не всегда. Достаточно добавить одно CSS свойство — знаменитый среди верстальщиков «нулевой хак» transform: translateZ(0), чтобы ускорить отрисовку в два раза.

    На этом можно было бы остановиться, но я увидел у слайда CSS свойство background-size: cover. Это свойство растягивает картинку на всю площадь блока. Зачем это было сделано остается тайной, т.к. высота блока фиксирована. Так что свойство не делало ничего, кроме ресайза картинки с 600 до 540 px по высоте. Ресайз картинки — это очень дорогостоящая операция, поэтому я отключил это CSS свойство и все стало совсем хорошо. Вот так буквально за пару минут мы в 4 раза увеличили fps. Все эти изменения вошли в новую версию страницы.

    Я уже несколько раз упомянул fps — frames per second. Это частота, с которой меняется изображение на дисплее. На большинстве экранов она равна 60 Гц, т.е. картинка меняется за секунду 60 раз. Путем сложных математических вычислений приходим к выводу, что каждый фрейм у нас создается раз в 16.6 миллисекунд. Если у нас на странице есть какая-нибудь анимация, нам нужно, чтобы браузер успел создать и отдать новый фрейм за 16 мс. Если он перестает это делать (а мы виртуозно умеем мешать ему в этом), браузер начинает фреймы пропускать. Количество fps падает, плавность уменьшается.

    Вот примеры из таймлайна. Каждому цвету здесь соответствует свой процесс. Думаю, они многим знакомы: script, render, paint/composite. Виновник проблем у нас чаще всего последний, который отвечает за отрисовку и составление страницы.



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

    От разметки к изображению


    Допустим у нас есть простая страничка с вот таким html-кодом:

    <!DOCTYPE html>
    <html>
    <head>
        <title>Пиар самого себя</title>
        <meta charset="utf-8">
        <link rel="stylesheet" href="main.css">
    </head>
    <body>
    <section>
        <h1>Всем привет!</h1>
        <img src="i.jpg" alt="Я">
        <p>Я занимаюсь фронтенд разработкой.</p>
        <p>И это круто!</p>
    </section>
    <script src="main.js"></script>
    </body>
    </html>
    

    Выглядеть она будет следующим образом:



    Первое, что делает браузер — выделяет теги и строит из них дерево DOM:



    Паралельно парсится CSS, накладывается на DOM, получается RenderTree. Это дерево отображения, содержащее Render Objects — объекты, которые нам надо показать пользователю. Дерево рендера для этой страницы выглядит так:



    Оно похоже на дерево DOM с некоторыми отличиями. В нем могут быть псевдоэлементы, которых нет в дереве DOM. Здесь также отсутствуют некоторые узлы из DOM: секция head, тэги link, script, элементы с display: none — все, что нам не надо показывать. Мы можем добавить в CSS некоторый код, от которого DOM не изменится, а в Render Tree появятся новые элементы.

    После этого начинается компоновка (layout/reflow). Когда браузер скомпоновал элементы по странице, ему осталось заполнить все это пикселями. Тут нужно немного рассказать про растеризацию: перевод изображения из вектора в конечную сетку пикселей, которую браузер покажет пользователю. В Хроме этим занимается графическая библиотека Skia.



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

    Весь процесс отображения страницы браузером выглядит так: парсинг HTML, парсинг CSS, DOM Tree, Render Tree, компоновка и отрисовка. Причем, изначально viewport представлял собой один растр. Вот так иллюстрируют этот процесс разработчики Chromium:



    В разделяемой памяти лежал конечный битмап страницы. C помощью оконного API данный битмап отрисовывался в нужное окно браузера. В процессе скроллинга страницы появлялись ранее невидимые Render Objects, которые нужно показать. Браузер создавал и отрисовывал новый растр вьюпорта. Это неоптимально, т.к. при скроллинге не хочется ждать пока все новое будет растеризовано. Хочется двигаться по уже готовой странице. Разработчики браузеров подумали об этом раньше, чем я, и ввели новый процесс — compositing.

    Теперь страница делится на слои. Изначально это был один слой, соответствовавший всему документу. Этот слой растеризовывался, сохранялся в памяти и выводился на экран. В этом случае при скроллинге браузеру не нужно было заниматься растеризацией, он оперировал уже готовым изображением. Процесс размещения на экране готового растра называется compositing (композитинг). Этот процесс выполнялся силами CPU. Производительность заметно улучшилась, но разработчики решили еще немного оптимизировать процесс, перенеся композитинг на GPU.

    Аппаратное ускорение


    И они это сделали. Первыми об этом начали говорить Microsoft, они пообещали эту функцию в IE9. По факту же первым выстрелил Firefox в четвертой версии. Потом вышел IE9, а в течение года подтянулись и все остальные браузеры. На текущий момент все основные браузеры, включая мобильные, отображают страницы с ускоренным при помощи GPU композитингом. Что происходит в этом случае? Страница точно так же делится на слои, которые растризуются в текстуры и отправляются в GPU, где они хранятся в видеопамяти. Тут в дело вступает новая часть системы — compositor. Она инструктирует GPU о том, как собрать конечное изображение.



    Слои


    Слой (композитный слой) — это часть страницы, поддерево DOM. Он отрисовывается независимо и компонуется в GPU. Он может растягиваться, перемещаться, скрываться (через прозрачность) без отрисовки.

    Слои создаются не просто так. Существуют условия, встречая которые браузер выносит элемент в новый композитный слой.

    • Элементы с 3D-трансформацией.

    Яндекс

    478,00

    Как мы делаем Яндекс

    Поделиться публикацией
    Комментарии 21
      +7
      Хорошая статья. В наши дни фронт-енд разработчики начинают думать про оптимизацию и аппаратное ускорение лишь тогда, когда страница начинает заметно подлагивать (и появляются недовольные пользователи вследствие негативного UX) через десятки/сотни программных анимаций/эффектов/переходов. Я считаю, что возможности CSS3 (и использование много чего из CSS2) должны спасти мир от «раздувания» ПО в вебе, которое сейчас на критично высоком уровне. Иногда пара строк в CSS работает в разы лучше и проще, чем пара десятков оних в JS.
        +1
        Как раз дебажил приложение для Firefox OS на слабом смартфоне, там как раз left/top заменил на 3d трансформации, и стало намного лучше.
        Вот бы на пару недель раньше мне эту статью)
          –19
          Путем сложных математических вычислений приходим к выводу, что каждый фрейм у нас создается раз в 16.6 миллисекунд.

          Где же тег sarcasm? )
            +1
            Спасибо за статью и отдельный комплимент презентации. Хочется спросить про один странный баг со шрифтами, доставивший массу неудобств полгода назад. На Mac OS в Safari в некоторых местах начинал загадочным образом мигать текст — шрифт то становился тонким, то толстым, примерно каждую секунду так переключался. В Chrome тоже было, но гораздо менее заметно, а вот в Firefox нормально. Долго и безрезультатно бился с этим миганием, так и не смог исправить. Помимо своих проектов встречал на множестве других сайтов, даже у гугла. Так понимаю это и есть та проблема с блюром? Правильно ли я понимаю, что для исправления проблемы, надо было выделять блоки с текстом в отдельный композитный слой (z-index'ом), поскольку анимация у меня происходила только в баннере, а текст просто висел статичным, без всяких анимаций, но тем не менее мигал? К сожалению не могу найти тот злосчастный баннер, из-за которого мигал текст в некоторых блоках на сайте, чтобы испробовать советы из доклада.
              +9
              Скорее всего вы видели что-то типа этого: files.chikuyonok.ru/anim-demo/

              И в большинстве случаев причина — неправильная композиция слоёв. В данном примере слой .anim переносится на GPU для ускорения анимации. То есть для этого слоя создаётся отдельная 3D-плоскость со своей текстурой, которая и компонуется с остальной страницей (которая, к слову, тоже является своего рода 3D-плоскостью). Однако слой .text по z-index находится выше слоя .anim, и браузеру ничего другого не остаётся как вынести слой .text также на отдельную плоскость в GPU, чтобы на экране вы увидели именно то, что задумали.

              Соответственно, этот вынос слоя с текстом на GPU (вернее, особенность Safari отрисовки текста на прозначном фоне для GPU-слоя) и даёт тот самый артефакт, Обратите внимание, что этот артефакт появляется строго в начале и в конце анимации, то есть когда меняется 3D-композиция.

              Решений этой проблемы несколько:

              1. Перенести слой .text по z-index ниже слоя .anim, если дизайн это позволяет. В этом случае не будет происходить неявный вынос слоёв в 3D. Это самое правильное и оптимальное решение.
              2. Указать слою .text непрозрачный фон, например, белый. Так вы замаскируете артефакт, но не избавитесь от проблемы неявного выноса. А это именно проблема, так как вынос на GPU – это всегда отдельный repaint и отдельная память.
              3. Явно вынести слой .text на GPU, хоть тем же translateZ(0). Так вы сделаете артефакт постоянным, но он хотя бы не будет «моргать». И вы также не избавитесь от проблемы.


              Вообще, прежде чем выносить что-то на GPU нужно хорошо представлять себе, что это такое и как именно это работает в недрах системы. Не стоит полагаться, что добавление «волшебного» translateZ(0) также «волшебно» ускорит страницу. Да, анимация будет быстрее, однако при неправильной композиции вы рискуете получить кучу проблем на скролле страницы или на банальном ховере на ссылках.
                0
                Спасибо большое за подробный ответ. Одной загадкой в жизни меньше стало, это радует.

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

                Правильно ли я понимаю, что есть еще и четвертый вариант — убрать пересечние блока с текстом и блока с анимацией? Видимо не правильно, т.к. попробовал в примере сдвинуть кубик сильно вниз (top: 300px), но проблема осталась. Это значит слои не ограничиваются блоком, а занимают всю страницу?
                  +1
                  четвертый вариант — убрать пересечние блока с текстом и блока с анимацией?

                  Нет, пересечения тут ни при чем, вынос происходит именно по z-стэку. Потому что браузер не может знать заранее, будут ли блоки пересекаться, если вы, например, анимируете их через JS.
                  0
                  Эффект наблюдается только при включенном субпиксельном сглаживании шрифтов. При рендеринге шрифта в отдельный слой субпиксельное сглаживание выключается, потому что оно не возможно, если не известен цвет фона.
                +1
                На самом деле, приведённая схема работы браузеров (DOM Tree → Render Tree → …) справделива для webkit, gecko и blink; в Internet Explorer, а также в Servo, схема другая.
                  0
                  А есть где почитать или посмотреть об этом?
                    0
                    Нигде, насколько я знаю. Я об этом знаю постольку, поскольку мы обсуждали API для Render Tree, и представитель Мозиллы заметил, что не все браузеры имеют схожий воркфлоу.
                      0
                      Непонятно, в чём тогда могут быть различия. Ведь DOM — объектное дерево, строящееся из структуры HTML. Но для отображения его недостаточно, CSS определяет свои фрагменты (boxes), как в примере с псевдо-элементом ::after, отсюда и Render Tree.
                        0
                        Я не знаю деталей. Просто говорю, что попытки стандартизировать API для Render Tree и Box Tree проваливаются именно по этой причине.
                  +2
                  … я увидел у слайда CSS свойство background-size: cover. Это свойство растягивает картинку на всю площадь блока… Ресайз картинки — это очень дорогостоящая операция, поэтому я отключил это CSS свойство и все стало совсем хорошо.


                  Ресайз картинки — дорогая операция, но делается один раз на repaint. Поэтому проблема с производительностью не в других размерах у картинки, а в постоянной перерисовке блока. А это, скорее всего, из-за неправильной композиции.
                    +5
                    А вы можете написать статью о том, как записываете лекции, какую технику и технологии используете?
                    Сам работаю в ВУЗе, но у нас запись преподавателей проходит по-проще.
                      +2
                      Интересный вопрос, не встречали ли вы проблем при подключении translateZ(0) на сайте? у кого проблемы с драйверами на видюху, у них при заходе на страницу просто комп вырубается, у нас так много пользователей на линукс стали жаловаться на это :)
                        0
                        На самом деле иметь отдельные слои и уж отдельный rendering process совершенно не обязательно при наличии современных механизмов 2D rendering. Мой Sciter например с Direct2D backend рисует напрямую вызывая D2D примитивы как они есть. И что-то мне говорит что IE поступает также.

                        Промежуточные bitmap buffers (a.k.a. layers) нужны при определенных анимациях, это да. Но это редкое исключение.
                          +1
                          Из инструментов для мониторинга рендера вы используете только стандартные Chrom DevTools или какие-то плагины еще?
                            +3
                            Все верно.
                            Chrome дает мощнейшие инструменты для мониторинга процесса отрисовки и композитинга. Сюда же я отношу инструмент Tracing (chrome://tracing) в Chrome Canary. Пример пошаговой растеризации и отрисовки страницы и 3D модель слоев из презентации были сделаны с помощью этого инструмента.
                            По поводу Chrome DevTools есть хорошие видео от Paul Irish. На русском языке есть интересный доклад (свежак) с последнего FrontTalks в Екатеринбурге (19 сентября) от Романа Сальникова.
                            Упоминая последний FrontTalks не могу не оставить ссылку на изумительный доклад Вадима Макишвили из 2 слайдов.
                              0
                              36, да. Я видел его в трансляции. У самого такие же проблемы, частично он дал ответы.
                            0
                            И еще хотелось бы ваши комментарии касательно вот этой статьи
                            css-tricks.com/myth-busting-css-animations-vs-javascript/

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

                            Самое читаемое