image
Автор: Антон Реймер
В первой части статьи, основанной моем вебинаре, мы рассмотрели общие принципы работы браузера. Во второй — я сконцентрировал внимание на важных событиях: repaints и reflows — и на принципах работы event loop.

Repaints and reflows

При загрузке страницы, если она не пустая, всегда выполняется, как минимум, по одному reflow и repaint. Далее эти события возникают в следующих случаях:

1. Часть дерева отображения нуждается в перерасчете, т. е. у какого-то узла изменились ширина, высота или координаты. Вызывается событие reflow.

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

Если вызывается reflow, после него обязательно вызовется и repaint. Но обратное неверно: repaint может вызываться независимо от reflow.

Какие действия вызывают reflow и/или repaint

1) Добавление, обновление, удаление DOM-узла. Потому что при этих событиях нужно перерасчитывать дерево отображения.

Функции:
insertAdjacentHTML(),appendChild(), insertBefore(), removeChild(),replaceChild(),remove(),append()/prepend(), after()/before(), replaceWith()

Изменение свойств DOM-узла:
innerHTML,innerText, width, height, offsetTop, offsetLeft, offsetWidth, offsetHeight, scrollTop/Left/Width/Height, clientTop/Left/Width/Height

Запрос свойств DOM-узла (без его изменения):
(offsetTop, offsetLeft, offsetWidth, offsetHeight, scrollTop/Left/Width/Height, clientTop/Left/Width/Height

Не только вызов функций и изменение DOM-узлов может вызвать reflow, но и простой запрос некоторых свойств, к которым, в частности, относятся все офсет-свойства. Почему при этом необходима компоновка, я расскажу позже.

2) Скрытие DOM-узла с помощью display: none (reflow и repaint) или visibility: hidden (только repaint, потому что нет геометрических изменений).

3) Перемещение, анимация DOM-узла.
Меняются координаты анимируемого узла дерева отображения, также это может вызывать изменение размеров других узлов.

4) Добавление/изменение CSS

left, top,right,bottom,width,height
Если мы захотим изменить эти css-свойства, это тоже вызовет reflow.

5) Пользовательские действия: изменение размеров окна (resize), изменение шрифта, прокрутка(scroll), drag and drop.

6) Другое

— JS Scrolling: Скроллинг через скрипт и соответствующие ему свойства.
scrollByLines(), scrollByPages(), scrollHeight, scrollIntoView(), scrollIntoViewIfNeeded(), scrollLeft, scrollTop, scrollWidth

— Глобальные методы и события для объекта window:
getComputedStyle(), scrollBy(), scrollTo(), scrollX, scrollY

— Работа с SVG

Пример

var bstyle = document.body.style; // cache
 
bstyle.padding = "20px"; // reflow, repaint
bstyle.border = "10px solid red"; // another reflow and a repaint
 
bstyle.color = "blue"; // repaint only, no dimensions changed
bstyle.backgroundColor = "#fad"; // repaint
 
bstyle.fontSize = "2em"; // reflow, repaint
 
// new DOM element - reflow, repaint
document.body.appendChild(document.createTextNode('dude!'));



На примере этого скрипта мы видим, что, если поменять padding, придется пересчитать размеры прямоугольника. Соответственно, вызовутся reflow и repaint. Если мы поменяем border, то ему задастся новая ширина, поэтому также будут вызваны reflow и repaint. А если бы мы просто поменяли цвет border, фона или текста — вызвался бы только repaint. Меняем размеры шрифта — reflow и repaint.

Браузер компонует вместе несколько запросов на reflow и repaint, и выполняет их единожды, оптимизируя свою работу. Но некоторые действия вынуждают браузер выполнить эти события незамедлительно.

Свойства:
1. offsetTop, offsetLeft, offsetWidth, offsetHeight
2. scrollTop/Left/Width/Height
3. clientTop/Left/Width/Height
4. getComputedStyle(), or currentStyle in IE
При запросе этих свойств браузеру нужно выполнить перекомпоновку незамедлительно, т. к. они должны возвращать актуальную информацию. Поэтому всякий раз, когда мы запрашиваем эти свойства, происходят reflow и repaint.

Советы по оптимизации кода для учета repaint и reflow

I. Не меняйте стили элемента напрямую в коде, используйте css-классы для переключения внешнего вида элемента. Или используйте свойство cssText.

// bad
var left = 10,
    top = 10;
el.style.left = left + "px";
el.style.top  = top  + "px";
 
// better 
el.className += " theclassname";
 
// or when top and left are calculated dynamically...
 
// better
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";


Способы:
1. Переключить CSS-класс. Часто внешний вид блока или его состояние можно изменить с помощью класса. Этот вариант лучше, чем работа напрямую со стилями, потому что хоть это все равно вызовет reflow и repaint, но только один раз. Например, в данном случае там, в первом примере, произойдет reflow и repaint при изменении события left, и ещё один reflow и repaint при изменении события top. Но если бы у нас left и top были прописаны в css-свойстве в каком-то классе, то при добавлении этого класса, у нас бы произошел один reflow и repaint вместо двух.
2. То же самое касается свойства cssText. Если мы заменим в этом тексте все необходимые нам свойства на динамические, то у нас произойдет один reflow и repaint.

II. Объединяйте манипуляции с DOM и выполняйте их отдельно от обновления DOM:

Сделать это можно несколькими способами:
• Используйте documentFragment, который предназначен для оптимизации работы с DOM-узлами.
• Создайте клон DOM-узла, работайте с ним, а по окончании замените им оригинальный DOM-узел. Это также оптимизирует количество reflow и repaint.
• Используйте display: none (1 reflow, repaint), внесите 100 изменений, восстановите display (1 reflow, repaint). Это лучше, чем вызывать по одному reflow на каждое изменение из 100.

III. Не запрашивайте вычисляемые стили слишком часто. Это вынуждают браузер делать reflow и repaint. Приведем пример, как можно оптимизировать код в этом случае. Если вам нужно работать с ними, возьмите их один раз, закэшируйте в переменной (если есть какой-то определенный цикл, лучше работать с переменной, чем каждый раз брать свойства) и работайте с ней.

// no-no!
for(big; loop; here) {
    el.style.left = el.offsetLeft + 10 + "px";
    el.style.top  = el.offsetTop  + 10 + "px";
}
 
// better
var left = el.offsetLeft,
    top  = el.offsetTop
    esty = el.style;
for(big; loop; here) {
    left += 10;
    top  += 10;
    esty.left = left + "px";
    esty.top  = top  + "px";
}


IV. Думайте о дереве отображения и пытайтесь понять, как много изменений вы вызываете в нем.
На этом моменте я остановлюсь подробнее, потому что компоновка компоновке рознь. И одна компоновка (reflow — если кто забыл) может быть значительно более трудозатратой, чем другая. Рассмотрим такой пример.

<div class="five red">
    <div class="four yellow">
        <div class="three green">
            <div class="two blue">
                <div class="one purple">

                </div>
            </div>
        </div>
    </div>
</div>



Здесь представлен небольшой фрагмент html. Допустим, мы захотим вставить какой-нибудь DOM-узел в блок 1.



К чему это приведет? Во-первых, блок 1 должен будет перерасчитать свои размеры, потому что неизвестно, каких размеров новый вставляемый DOM-узел. Нужно понять, есть ли необходимость сделать ширину больше или меньше и т. д. Изменение размеров блока 1 вынуждает делать перекомпоновку блока 2. Это вызывает перекомпоновку блоков 3, 4 и 5. Т. е. мы видим, что операция получается трудозатратной. Но все будет иначе, если мы вставим наш DOM-узел, например, после четвертого DOM-узла.



В этом случае обновится только пятый DOM-узел, и компоновка таким образом получается гораздо более быстрой.
Еще один наглядный пример — работа с анимацией. Допустим, мы хотим анимировать перемещение блока 1 на 200 пикселей вправо. Если блок 1 имеет position: static, то есть он находится в стандартном потоке компоновки и учитывается другими блоками, то изменение его координат будет производить перекомпоновку всех его предков. Если же блок 1 будет иметь position: absolute, он выйдет из стандартного потока компоновки. В этом случае изменение его координат будет требовать перекомпоновки только отдельных его предков. Допустим, у прямоугольника 5 стоит position: relative, и наш прямоугольник 1 будет компоноваться относительно блока номер 5. Тогда изменение блоком 1 своих координат, будет вызывать компоновку только пятого DOM-узла.

Event loop

Рассмотрим тему асинхронных событий и Event loop, т. к., на мой взгляд, она часто вызывает недопонимание.

"

Что такое event loop? Это бесконечный цикл, который реализуется JS-движком (V8, JavaScriptCore и т. п.) и предназначен для правильного выполнения скрипта, в частности асинхронных событий. JS-движок не стоит путать с браузерными движками, к которым относится Webkit и Gecko. Также не стоит путать между собой event loop и цикл работы браузера.

На клиентской стороне есть три типа асинхронных действий:
1. Таймер.

setTimeout(function timerFn(){
    console.log('timerFn');
},100)


2. Ajax
$.ajax({
  url: 'someUrl',
  success: function ajaxFn(data) {
        console.log('ajaxFn')
  }
});


3. Пользовательское действие.

$('something').on('click', 
    function clickFn(){
        console.log('clickFn')
    }
)


Все эти асинхронные события имеют дело с callbacks. Механизм callbacks позволяет обеспечить правильную работу асинхронных событий. Посмотрим на небольшой фрагмент кода. У нас есть callback ClickFn(). Он выполняется, когда мы кликаем на элементе something. Допустим, в это время у нас в call stack выполняются какой-то очень большой код. clickFn попадет в очередь callbacks и будет ждать там своей очереди.

Допустим, следующим у нас возвращается response oт сервера — соответствующий callback ajaxFn встает в свою очередь. После этого у нас срабатывает таймер, и его callback также встает на свое место в этой очереди. Кстати, разработчику нужно понимать, что указанное им в качестве задержки для таймера время — приблизительное количество миллисекунд. Потому что браузеру нужно будет учесть все callbacks, которые уже стоят в очереди callbacks. Если, скажем, callback clickFn и ajaxFn будут выполняться по миллисекунде каждый, callback таймера выполнится не через 100 миллисекунд, как заявлено в скрипте, а через 102. Впрочем, в обычной ситуации такими погрешностями можно пренебречь.

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



Когда наш код в Call Stack перестал выполняться, браузер может выполнить код первого callback из очереди callbacks. Собственно, выполняется clickFn. Когда он выполнен, место освобождается. За ним выполняется следующий callback — ajaxFn, далее — timerFn, и очередь оказывается пустой. Это и есть упрощенная схема работы event loop.

Очередь callbacks позволяет им выполняться не параллельно, а последовательно, друг за другом. Так как в callback, мы можем работать с одними и теми же переменными, с одними и теми же DOM-узлами, их параллельное, т. е. асинхронное выполнение вызовет конфликты. В этом случае браузер не сможет разобраться, что именно нужно делать.

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

То же можно сказать и по поводу потоков. JavaScript выполняется в одном потоке, который и работает с DOM. По большому счету, нет смысла в создании нескольких параллельных потоков — их наличие создает дополнительную путаницу и затрудняет работу с DOM. Но способ организовать параллельные потоки для скрипта все же существуют: использовать Web Worker — технологию, которая появилась в стандарте html5. У нее есть одно ограничение — Web Worker не может работать напрямую с DOM и должен взаимодействовать с основным потоком путем обмена сообщениями.

Источники:
1. http://www.html5rocks.com/ru/tutorials/internals/howbrowserswork/
2. http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/
3. http://2014.jsconf.eu/speakers/philip-roberts-what-the-heck-is-the-event-loop-anyway.html
Поделиться с друзьями
-->

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


  1. ThisMan
    06.07.2016 23:20

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

    А потом вы пишите, что
    > Если у нас выполняется AJAX, следующий за ним код все равно будет выполняться — он не ждет ответа от сервера.

    Не совсем понял, это оговорка или вы сами себе противоречите?


    1. DataArt
      06.07.2016 23:31

      Да, извините, здесь была опечатка, допущенная непосредственно при публикации. Конечно, не блокирующий, спасибо!


  1. reh4er
    07.07.2016 12:05

    Было бы интересно знать, насколько сокращается время загрузки страницы при минимизации repaints/reflows


    1. Murmurianez
      07.07.2016 21:22

      Их оптимизируют для плавности анимации, в любом адекватном случае влиянием на скорость загрузки (первоначальной отрисовки) можно пренебречь.


  1. spaceinvaider
    07.07.2016 18:18

    >На клиентской стороне есть три типа асинхронных действий:
    это не все, еще есть — браузерные события(onload etc), Промисы, сообщения от веб воркеров


  1. tenbits
    07.07.2016 19:22
    +1

    I. Не меняйте стили элемента напрямую в коде, используйте css-классы для переключения внешнего вида элемента. Или используйте свойство cssText.

    А разве reflow это не отложенное действие, которое откладываеться на конец ивент лупа или до принудительного перерасчета? То-есть, изменяя свойства top, left произойдет лишь один reflow, как и в случаях с css классом и cssText.


    1. Large
      10.07.2016 11:07

      В этом случае отложенное, об этом даже в статье говорится:

      Браузер компонует вместе несколько запросов на reflow и repaint, и выполняет их единожды, оптимизируя свою работу.


      Так что в этом случае так лучше не делать — это плохо читается и никак не оптимизирует процесс.


    1. Large
      10.07.2016 11:11

      II. Объединяйте манипуляции с DOM и выполняйте их отдельно от обновления DOM:


      Уже тоже не слишком актуально. Браузеры уже научились это вполне сносно оптимизировать и если раньше сортировка таблицы вне дом с последующей вставкой занимала в разы меньше времени, чем та же операция в дом, то сейчас разница не так заметна (можно посмотреть приер у Ильи Кантора).

      Тем не менее этот пунк все равно правильнее сделать, но не стоит рассчитывать на заметный прирост производительности.


    1. Large
      10.07.2016 11:15

      III — тоже именно в этом примере (где можно вынести вычисления за цикл) разницы в современных браузерах не будет.


    1. Large
      10.07.2016 11:23

      IV — анимировать позицию с помощью изменения box model в принципе плохая идея которая в любом случае будет просаживать производительность на мобильных устройствах. Для этого есть более удачные способы с использованием видеокарты translate3d, например.


  1. Stalker_RED
    07.07.2016 22:34

    Дополню, что в браузерах есть профайлеры, которые могут помочь выяснить отчего же ваш сайт подтормаживает.
    http://i.imgur.com/eOwDrc7.png
    https://developers.google.com/web/tools/chrome-devtools/profile/evaluate-performance/timeline-tool#profile-js


  1. domix32
    07.07.2016 23:38

    Классная статья, только изображения сильно пошакалило