Пользователь
0,0
рейтинг
26 мая 2014 в 20:08

Разработка → Рендеринг WEB-страницы: что об этом должен знать front-end разработчик из песочницы

Приветствую вас, уважаемые хабравчане! Сегодня я бы хотел осветить вопрос рендеринга в веб-разработке. Конечно, на эту тему уже написано много статей, но, как мне показалась, вся информация довольно разрознена и отрывочна. По крайней мере, чтобы собрать всю картину в своей голове и осмыслить её, мне пришлось проанализировать немало информации (в основном — англоязычной). Именно поэтому я решил формализовать свои знания в статью, и поделиться результатом с сообществом Хабра. Думаю, информация будет полезна как начинающим веб-разработчикам, так и более опытным, чтобы освежить и структурировать свои знания.

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

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

Процесс обработки WEB-страницы браузером


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

  1. Из полученного от сервера HTML-документа формируется DOM (Document Object Model).
  2. Загружаются и распознаются стили, формируется CSSOM (CSS Object Model).
  3. На основе DOM и CSSOM формируется дерево рендеринга, или render tree — набор объектов рендеринга (Webkit использует термин «renderer», или «render object», а Gecko — «frame»). Render tree дублирует структуру DOM, но сюда не попадают невидимые элементы (например — <head>, или элементы со стилем display:none;). Также, каждая строка текста представлена в дереве рендеринга как отдельный renderer. Каждый объект рендеринга содержит соответствующий ему объект DOM (или блок текста), и рассчитанный для этого объекта стиль. Проще говоря, render tree описывает визуальное представление DOM.
  4. Для каждого элемента render tree рассчитывается положение на странице — происходит layout. Браузеры используют поточный метод (flow), при котором в большинстве случаев достаточно одного прохода для размещения всех элементов (для таблиц проходов требуется больше).
  5. Наконец, происходит отрисовка всего этого добра в браузере — painting.

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

Repaint


В случае изменения стилей элемента, не влияющих на его размеры и положение на странице (например, background-color, border-color, visibility), браузер просто отрисовывает его заново, с учётом нового стиля — происходит repaint (или restyle).

Reflow


Если же изменения затрагивают содержимое, структуру документа, положение элементов — происходит reflow (или relayout). Причинами таких изменений обычно являются:

  • Манипуляции с DOM (добавление, удаление, изменение, перестановка элементов);
  • Изменение содержимого, в т.ч. текста в полях форм;
  • Расчёт или изменение CSS-свойств;
  • Добавление, удаление таблиц стилей;
  • Манипуляции с атрибутом «class»;
  • Манипуляции с окном браузера — изменения размеров, прокрутка;
  • Активация псевдо-классов (например, :hover).


Оптимизация со стороны браузера


Браузеры по возможности локализуют repaint и reflow в пределах элементов, подвергнувшимися изменению. Например, изменение размеров абсолютно или фиксировано спозиционированного элемента затронет только сам элемент и его потомков, в то время как изменение статично спозиционированного — повлечет reflow всех элементов, следующих за ним.

Ещё одна особенность — во время выполнения JavaScript браузеры кэшируют вносимые изменения, и применяют их в один проход по завершению работы блока кода. Например, в ходе выполнения данного кода произойдет только один reflow и repaint:

var $body = $('body');
$body.css('padding', '1px'); // reflow, repaint
$body.css('color', 'red'); // repaint
$body.css('margin', '2px'); // reflow, repaint
// На самом деле произойдет только 1 reflow и repaint

Однако, как описано выше, обращение к свойствам элементов вызовет принудительный reflow. То есть, если мы в приведённый блок кода добавим обращение к свойству элемента, это вызовет лишний reflow:

var $body = $('body');
$body.css('padding', '1px');
$body.css('padding'); // обращение к свойству, принудительный reflow
$body.css('color', 'red');
$body.css('margin', '2px');

В итоге мы получим 2 reflow вместо одного. Поэтому, обращения к свойствам элементов по возможности нужно группировать в одном месте, дабы оптимизировать производительность (см. более подробный пример на JSBin).

Но, на практике встречаются ситуации, когда без принудительного reflow не обойтись. Допустим, у нас есть задача: к элементу нужно применить одно и то же свойство (возьмём «margin-left») сначала без анимации (установить в 100px), а затем — анимировать посредством transition в значение 50px. Можете сразу посмотреть этот пример на JSBin, но я распишу его и тут.
Для начала заведём класс с transition:

.has-transition {
   -webkit-transition: margin-left 1s ease-out;
      -moz-transition: margin-left 1s ease-out;
        -o-transition: margin-left 1s ease-out;
           transition: margin-left 1s ease-out;
}

Затем, попробуем реализовать задуманное следующим образом:

var $targetElem = $('#targetElemId'); // наш элемент, по умолчанию у него присутствует класс "has-transition"

// убираем класс с transition
$targetElem.removeClass('has-transition');

// меняем свойство, ожидая, что transition отключён, ведь мы убрали класс
$targetElem.css('margin-left', 100);

// ставим класс с transition на место
$targetElem.addClass('has-transition');

// меняем свойство
$targetElem.css('margin-left', 50);

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

// убираем класс с transition
$(this).removeClass('has-transition');

// меняем свойство
$(this).css('margin-left', 100);

// принудительно вызываем reflow, изменения в классе и свойстве будут применены сразу
$(this)[0].offsetHeight; // как пример, можно использовать любое обращение к свойствам

// ставим класс с transition на место
$(this).addClass('has-transition');

// меняем свойство
$(this).css('margin-left', 50);


Практические советы по оптимизации


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

  • Пишите валидный HTML и CSS, с указанием кодировки. Стили лучше включать в <head>, а скрипты — в конце <body>.
  • Стремитесь упрощать и оптимизировать селекторы CSS (этим часто пренебрегают разработчики, использующие препроцессоры). Чем меньше вложенность — тем лучше. По эффективности обработки селекторы можно расположить в следующем порядке (начиная с наиболее быстрого):
    1. Идентификатор: #id
    2. Класс: .class
    3. Тэг: div
    4. Соседний селектор: a + i
    5. Дочерний селектор: ul > li
    6. Универсальный селектор: *
    7. Селектор атрибутов: input[type="text"]
    8. Всевдоэлементы и псевдоклассы: a:hover

    Следует помнить, что браузер обрабатывает селекторы справа налево, поэтому в качестве ключевого (крайнего правого) селектора лучше использовать наиболее эффективные — идентификатор и класс.
    div * {...} // плохо
    .list li {...} // плохо
    .list-item {...} // хорошо
    #list .list-item {...} // хорошо
    

  • В скриптах минимизируйте любую работу с DOM. Кэшируйте всё: свойства, объекты, если подразумевается повторное их использование. При сложных манипуляциях разумно работать с «offline» элементом (т.е. который находится не в DOM, а в памяти), с последующим помещением его в DOM.
  • При использовании jQuery для выборки элементов придерживайтесь рекомендаций по составлению селекторов.
  • Для изменения стилей элементов лучше модифицировать только атрибут «class», и как можно глубже в дереве DOM, это и более грамотно с точки зрения разработки и поддержки (отделение логики от представления), и менее затратно для браузера.
  • Анимировать желательно только абсолютно и фиксировано спозиционированные элементы.
  • Можно отключать сложные :hover анимации во время скроллинга (например, добавляя к body класс «no-hover»). Статья на эту тему.


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


Надеюсь, каждый читатель извлёк из статьи что-нибудь полезное. В любом случае — спасибо за внимание!

UPD: Спасибо SelenIT2 и piumosso за верные замечания по поводу эффективности обработки CSS-селекторов.
Скутин Александр @skutin
карма
19,0
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

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

  • +8
    How browsers work есть на русском, если что. Даже была статья на Хабре.
  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      В этом аспекте я, признаться, допустил оплошность — в действительности будет так:
      .list-item {...} // хорошо
      #list-1 .list-item {...} // немного медленнее, т.к. ключевой селектор совпадает с предыдущим стилем, но добавлена вложенность
      

      Привычка к инкапсуляции уже работает на автомате. Дополнил этот пункт в статье, спасибо за замечание!
  • –1
    #list-1 .list-item {...} // идеально
    #list-1 > .list-item {...} // ещё идеальнее
    
    • +2
      css селекторы по id — никогда не было идеальным. Это примерно как goto, использовать можно, но не нужно, пусть хоть и незначительно быстрее.
    • 0
      Каскадность и идентификаторы в качестве селекторов — это ни разу не идеально, а плохо.
      Тем не менее, каскад до трёх элементов можно допустить, но только в случае особой необходимости. В остальных случаях это bad practice.
  • 0
    #list-1 .list-item {...} // идеально

    А почему идеально? Разве браузер будет разбирать это слева направо?
    • +2
      Да, вы правы, браузер обрабатывает css справа налево, и идеальным с точки зрения производительности этот вариант назвать нельзя. Дал более развёрнутый ответ выше.
  • 0
    Интересно, почему универсальный селектор (*), работает быстрее селектора по аттрибуту или псевдоэлементов?
    По логике, он должен быть самым медленным.
    • 0
      Ну как, звездочка выбирает все элементы вниз по дереву. Селектор по аттрибуту делает то же самое, но еще проверяет аттрибут у каждого найденного элемента — в два раза больше необходимых действий даже не зная алгоритмов селекции.
      • 0
        Но ведь стилизовать все элементы будет не очень быстро.
        • 0
          В данном случае речь именно про селектор, т.е. поиск элементов, а не про какие-либо изменения в стилях.
  • +1
    Подобные статьи летают из блога в блог, но существуют тесты которые подтверждают пользу от такой оптимизации для современных браузеров? Речь не об искусственных тестах на абсурдно сложном DOM и тысяче селекторов по атрибуту.
    • +2
      Мне кажется, тут речь не о том, что нужно бежать всё оптимизировать, а о том, что нужно думать головой, когда пишешь код.
      • 0
        Я говорю о блоке «Практические советы по оптимизации». Сколько в этом реальной пользы?
        • 0
          Польза есть. Я, например, и так до этого знал, что не очень хорошо допускать высокий уровень вложенности — просто мне напомнили об этом и я стараюсь впредь следить за своим scss лучше.
          • 0
            Вопрос в том, станет ли большая вложенность причиной заметных тормозов на реальном проекте. Если нет, то почему это «не очень хорошо»?
            • +1
              Могу точно заверить, что причина падения производительности в мобильных приложениях или сайтах, на мобильных устройствах — это html и css, отнюдь не js (даже на десятки тысяч строк), как бы это могло бы показаться. Так что если отвечать на этот вопрос — да, могут стать причиной и облегчение подобных css правил вполне могут в некоторой степени ускорить работу приложения.

              Небольшой оффтоп: Более того — в основном причины падения производительности — это модные css3 фишки: transition, transform, border-radius, box-shadow, rgba, filter, linear\radial-gradient и прочее. Приходится поднапрячь память и начать верстать скруглённые уголки по-старинке, пиля картиночки и расставляя их в ламповую табличку.
              • НЛО прилетело и опубликовало эту надпись здесь
                • +1
                  Cкругления — это ведь Безье + антиалиазинг, которые генерируются, а картиночки — тупо рисование поверх. При тех макетах страничек, что у нас на работе — разница видна (на замерах, глазу не особо заметно — что так тормозит, что так :D ). Но если брать целостные оптимизации (избавление от комплекса стилей) — скорость возросла заметно, раньше вообще всё еле двигалось.
                  • НЛО прилетело и опубликовало эту надпись здесь
                    • +1
                      Помнится на хабре уже были замеры по этому поводу и более-менее сложный SVG бывало тормозил даже в обычных браузерах, уж я не говорю про мобильные.
                  • 0
                    Какую методику использовали для замеров? Можете вкратце описать?
                    • +1
                      Weinre + Timeline
                      • 0
                        Ага, спс. А я вот как раз подумывал о Performance Log из Chromedriver2.
              • 0
                Про «модные css3 фишки» я в курсе (border-radius к ним бы точно не относил), но вот селекторы… как по мне это экономия на спичках. Особенно на мобильных сайтах, там DOM как правило небольшой.
                • +1
                  Да, я ответил выше, что хоть скругления — это такие же мелочи, как и использование id, вместо классов, но комплексное решение — действительно ускоряет (тем более при наличии должного самописного миксина в scss для подобных нужд — это делается с пол пинка).
                  • 0
                    Со скруглениями мороки много, это же придётся ещё и под весь зоопарк dpi нарезать. Мне для этого нужны весомые аргументы)
                    • 0
                      Пока что (во время разработки) спасает background-size для изображений x2 размера — это позволяет использовать одни и те же картинки, как для retina, так и для девайсов со стандартной плотностью пикселей. Но потом, конечно можно заменить на размеры 1 к 1 для ускорения (масштабирование тоже откушивает довольно сильно).

                      Буквально только сегодня набросал: pastebin.com/jqs95q2C но для идеального решения — проще проставлять общий класс ".sprite-x1\.sprite-x2" в html\body тегах и от него отталкиваться при отображении изображений из спрайтов =) Вот
                      • 0
                        Не думаю что получится добиться идеального результата, границы всё равно будут «мылить», особенно при зуме. Градиентов это тоже касается.
                        • 0
                          Согласен, с другой стороны не всё так плохо, как могло бы быть при ресайзе с увеличением (размытием пикселей).

                          Ниже скрин андроида с двухкратным уменьшением (на айфончиках всё было бы 1к1):
                          Скрытый текст
                          картинками:
                          — иконка с человечком
                          — тёмно-красная полосочка ниже (она из двух картиночек)
                          — красная полосочка с засечками ещё ниже (она состоит из 3х картиночек)
                          — иконка с кулаком


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

    • 0
      Подтверждение от гитхаб speakerdeck.com/jonrohan/githubs-css-performance
    • 0
      Избавление от вложенности даёт не столько выигрыш в производительности, сколько удобство будущей поддержки – если у вас изменится DOM, например, меню из заголовка куда-то подвинули, не надо будет переписывать все селекторы типа .head .menu .list .item {…}
  • +1
    А на чем, собственно, базируются доводы из статьи и скорости работы с селекторами?

    Интереса ради решил проверить, получилось вот что:

    image

    В первом столбце показывается время отработки селектора из второго столбца в миллисекундах. Поиск происходил по 100000 элементов «div» в body, при 100 итерациях на каждом селекторе.

    Пример скрипта (результат выведется в консоль браузера, поддерживающего console.table()) (количество итераций уменьшено до 10000).

    Как видно из таблицы скорость отработки различных селекторов по отношению друг к другу не совпадает с той, что указана в статье.
    • 0
      Небольшая поправка. Количество итераций — 100, а количество элементов, среди которых происходит поиск — 100000 в таблице, и 10000 в примере.
    • НЛО прилетело и опубликовало эту надпись здесь
      • 0
        Да, id уникальный, class дублировался. В итоге просто по id только один элемент нашелся, а по class'у несколько.
        • НЛО прилетело и опубликовало эту надпись здесь
          • 0
            можно попробовать и Id'ы сделать не уникальными. Браузеры их все находят, а не только один (то-есть если создать два элемента с одним ID, то .querySelectorAll() вернет оба элемента).
        • НЛО прилетело и опубликовало эту надпись здесь
          • 0
            Универсальный селектор не тормозит. Просто «querySelectorAll» находит все элементы по нему. Попробуйте изменить на «querySelector» и результат будет уже иной:

            Chrome 37.0.2020.0 canary
            image


            Еще можно попробовать использовать псевдо-кдасс :only-of-type и т.п.
            • НЛО прилетело и опубликовало эту надпись здесь

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