Если я общаюсь с джуниор разработчками, всегда интересно узнать, зачем по их мнению мы используем фреймворки в повседневной разработке? Мне нравится когда люди не просто используют какой-то инструмент потому что "так надо", а еще и знают его плюсы и минусы. В первую очередь фреймворки используют потому что большие приложения написанные на ванильном JS получают по большей степени сильно не оптимизированными. DOM дерево - это большой обьект, и каждое взаимодействие с ним неплохо нагружает браузер, в связи с этим в фреймворках и появилась такая вещь как Virtual DOM Легковесная копия ДОМ дерева, созданная для того чтобы производить все манипуляции (read/write/update/delete) с ней, а в основной DOM вносить уже точечные изменения.
В этой статье я расскажу почему именно операции с DOM становятся тяжеловесными и как их оптимизировать.
Рассмотрим пример
Приложение на скрине ниже рендерит в div с id app 1000 div элементов с рандомным текст контентом. Сейчас особо никаких проблем нет

Единственное, в JS один поток, соответственно пока эта функция работает, интерфейс заблокирован, а мы ведь хотим работать с отзывчивым интерфейсом, соответственно чем быстрее она выполнится, тем лучше.
Понимаем проблематику
В браузере происходят такие стадии отрисовки контента как Reflow и Repaint. Они происходят на этапе первого рендеринга страницы и во время жизненного цикла. Repaint - довольно легковесная операция, которая вызывается например при изменении каких-то ксс свойств, она просто немного перекрашивает страницу и не забирает много производительности. В отличие от Repaint, Reflow забирает много производительности, Reflow вызывается:
- При изменении размера окна
- При считывании стилей элемента
- При удалении либо добавлении элементов в DOM дерево
Reflow каждый раз заново пересчитывает размеры окна и так же размеры и позицию каждого элемента, это сложное вычисление которое нагружает браузер
Приведу пример случая, когда Reflow вызывается лишний раз
Тут вызов Reflow будет один раз, производится так называемая WRITE операция - запись стилей элемента
div.style.color = 'blue';
div.style.marginTop = '30px';Здесь вызов Reflow произойдет дважды, обьясняю почему Write операция записи стилей элемента DIV прервана READ операцией - считыванием стилей. До момента этой операции все чем занимался браузер - это Repaint, он просто менял стили и ему не нужен был лишний рефлоу, он сделает его когда завершит Write операции, полностью изменит стили Но юзер как бы форсирует Reflow потому что просит у браузера посчитать высоту элемента когда браузер еще не закончил менять стили, и ему приходится произвести пересчет размеров и позиций всех элементво чтобы дать юзеру актуальную информацию
div.style.color = 'blue';
const margin = div.getBoundingClientRect().height;
div.style.marginTop = (margin + 10) + 'px';Один из способов оптимизации read/write оптимизаций с дом деревом - некий batching То есть вам рекомендуется производить сначала все write а потом все read операции, не прерывая их, чтобы не вызывать лишние Reflow
// Этот способ быстрее
const width = element.clientWidth + 10;
const width2 = element.clientWidth + 20;
element.style.width = width + 'px';
element.style.width = width2 + 'px';
// Чем этот
const width = element.clientWidth + 10;
element.style.width = width + 'px';
const width2 = element.clientWidth + 10;
element.style.width = width2 + 'px';Другие способы оптимизации:
- Разделение батчингом
- Можно пробовать изменять все стили за один раз добавляя css class
- Используем document.fragment как временное решение, генерируем все что нам нужно туда, а потом один раз в body добавляем фрагмент (сниппет кода ниже)
- Как вариант можно задать элементу display: none чтобы он не влиял на layout страницы и каждое его изменение не вызывало reflow (кроме одного когда меняется дисплей), потом произвести с ним все js манипуляции и снова сетать display: block
Сниппет к 4 пункту, тут тоже мы каждый раз вызываем лишний Reflow когда добавляем элемент в DOM
// Неверно
for(let i = 0; i < 100; i++){
const element = document.createElement('div');
document.body.appendChild(element);
}
// Верно
const fragment = document.createDocumentFragment();
for(let i = 0; i < 100; i++){
const element = document.createElement('div');
fragment.appendChild(element);
}
document.body.appendChild(fragment);Benchmark приложения
Представим что зачем-то у нас появилась необходимость собрать ширину каждого div, а она у нас разная тк я задал в styles.css свойство
div {
width: min-content;
}Теперь приложение будет выглядеть так, сразу же добавлю строчки для замера времени выполнения функции

Результаты перфоманса которые мы видим в консоли могут довести разработчика до обморочного состояния, на выполнение функции уходит 130-140мс, учитывая что без замера width на нее уходит +-3мс

Решение
Выше я уже предлагал способы решения этой проблемы, я выберу тот - который я назвал "batching"
Сначала произведу все Write операции, а потом все Read, тогда не будет происходить так называемый forced reflow который сильно влияет на перфоманс

Делаем код вот таким и получаем перфоманс как на скрине ниже

Спасибо всем за внимание, не забывайте следить за обращением к DOM дереву, не обращайтесь к нему лишний раз либо старайтесь оптимизировать эти действия, иначе интерфейс вашего приложения будет заблокирован для юзера надолго :)