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

ДОМ

Из определения MSDN для DOM:

Объектная модель документа (DOM) — это представление данных объектов, составляющих структуру и содержимое документа в Интернете. В этом руководстве вы познакомитесь с DOM, посмотрите, как DOM представляет документ HTML в памяти и как использовать API для создания веб-контента и приложений.

DOM — это независимый от языка интерфейс приложения для работы с документами HTML и XML. Будучи независимыми, браузеры сохраняют реализацию DOM и JavaScript независимо друг от друга.

Это разделение позволяет другим технологиям и языкам, таким как VBScript, использовать DOM и функции рендеринга, которые может предложить Trident.

Например:

  • Safari использует WebCore WebKit для DOM и рендеринга, хотя у него есть отдельный движок JavaScriptCore.
  • Google Chrome также использует библиотеки WebCore из WebKit для рендеринга страниц, но реализует собственный механизм JavaScript под названием V8.
  • Firefox использует движок рендеринга Gecko и SpiderMonkey в качестве движка JavaScript.

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

Рассмотрим этот пример:

function loop1() {
  let start = performance.now();
  
  for (let i = 0; i < 1500; i++) {
    document.getElementById('spanElement').innerHTML += 'text';
  }
  
  let end = performance.now();
  console.log(`Loop1 execution took ${end - start} ms`);
}

function loop2() {
  let start = performance.now();
  
  let contentToAppend = '';
  for (let i = 0; i < 1500; i++) {
    contentToAppend += 'text';
  }
  document.getElementById('spanElement').innerHTML += contentToAppend;
  
  let end = performance.now();
  console.log(`Loop2 execution took ${end - start} ms`);
}

В loop1 мы обращаемся к DOM 2 раза в каждом цикле, один раз для чтения содержимого HTML (innerHTML) и второй раз для добавления текста.
Принимая во внимание, что в loop2 мы просто сохраняем обновленное содержимое и записываем значение в конец петли. Вполне естественно, что loop2 намного быстрее, чем loop1

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

function slow() {
  let columns = document.getElementsByTagName('div');
  let name = '';

  for(let i = 0; i < columns.length; i++) {
    // accessing DOM element.
    name = document.getElementsByTagName('div')[i].nodeName;
    name = document.getElementsByTagName('div')[i].nodeType;
    name = document.getElementsByTagName('div')[i].tagName;
  }
  return name;
}


function fast() {
  // caching the results in a local variable.
  // Also, querySelector is faster than getElementsByTagName.
  // can you tell why ? 
  let columns = document.querySelectorAll('div');
  let name = '';
   
  for (let i = 0, len = coll.length; i < len; i++) {
    let element = columns[i];
    // accessing local variable is much faster than arrays.
    name = element.nodeName;
    name = element.nodeType;
    name = element.tagName;
  }
  return name;
}

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

Перерисовка и оплавление

После того, как браузер загрузил все компоненты страницы, то есть HTML, JavaScript, таблицы стилей, изображения и т. д., он сохраняет информацию в двух внутренних структурах данных:

Дерево DOM, представляющее структуру страницы.

Дерево рендеринга показывает, как будут отображаться элементы DOM, таким образом, содержит всю информацию о стиле элементов. Дерево рендеринга имеет по крайней мере один узел для каждого узла в дереве DOM (за исключением скрытых узлов), и каждый узел/блок/кадр хранит информацию об отступах, полях, границах, положении и т. д. Когда изменение DOM влияет на геометрию элемента , браузер должен пересчитать геометрию элемента, а также геометрию других элементов, затронутых изменением. Таким образом, часть дерева рендеринга должна быть признана недействительной и перестроена. Этот процесс называется оплавлением.

После завершения перекомпоновки браузер перерисовывает затронутые части экрана в процессе, называемом перерисовкой.

Не все изменения DOM влияют на геометрию, например, изменение цвета фона узла не приводит к перекомпоновке, а вызывает перерисовку затронутой части.

Когда происходит перепрошивка?

  • Добавляются или удаляются видимые элементы DOM.
  • Размер/позиция/содержимое элемента изменены.
  • Страница отображается изначально.
  • Размер окна браузера изменен.

Из-за затрат, связанных с перекомпоновкой, браузер часто ставит эти изменения в очередь, а затем выполняет обновления пакетами. Однако эту очередь необходимо очищать всякий раз, когда запрашивается какая-либо информация о макете, например:

  • offsetTop, offsetLeft, offsetWidth, offsetHeight
  • scrollTop, scrollLeft, scrollWidth, scrollHeight
  • clientTop, clientLeft, clientWidth, clientHeight

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

Минимизация перерисовки и перекомпоновки

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

// inefficient
function updateStyle_slow() {
  let element = document.getElementById('navPanel');
  element.style.border = '1px';
  element.style.borderColor = 'red';
  element.style.padding = '3px';
}

// good practice
function updateStyle_fast1() {
  let element = document.getElementById('navPanel');
  element.style.cssText = 'border: 1px; border-color: red; padding: 3px';
}

// or best to use css class
function updateStyle_fast2() {
  let element = document.getElementById('navPanel');
  element.classList.add('red-border');
}

Когда у нас есть много изменений, которые нужно применить к элементу DOM, мы можем уменьшить количество перерисовок и перекомпоновки:

  1. Отделение элемента от документа.
  2. Применить изменения.
  3. Добавьте элемент обратно в документ.

Этот процесс вызовет перекомпоновку на шаге 1 и шаге 3. Для достижения этих шагов мы можем сделать одно из следующих действий:

  • Скройте элемент, примените изменения и снова отобразите его (используя display:hidden и display:block стили CSS).
  • Используйте фрагмент документа для построения поддерева вне DOM, а затем скопируйте его в документ. Перейдите по ссылке для получения дополнительной информации о фрагментах документа.
  • Скопируйте исходный элемент во внедокументный узел, измените копию, а затем замените исходный элемент обратно.

Заключение

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

Взгляните на эту ссылку, чтобы узнать больше о профилировании производительности в консоли разработчика: https://www.youtube.com/watch?v=KWM5wxlDuis