Обычный, теневой, виртуальный, инкрементальный… Как получилось, что простой программный интерфейс доступа к элементам веб-страниц обзавелся таким количеством «родственников»? Чем современные фреймворки не устраивает стандартная объектная модель документа или просто DOM? Что и как на самом деле отрисовывает браузер в процессе рендера веб-страницы?
Всем привет, это Макс Кравец из Holyweb. Помните сцену из Матрицы, в которой один из юных кандидатов в Избранные наставляет Нео: «Не пытайся согнуть ложку. Первое, что ты должен понять — ложки не существует!»? Давайте переформулирую: «Не пытайся изменить DOM...». А вот о том, что прячется под многоточием, мы сегодня и поговорим.
Фундамент. Как строится веб-страница. CRP
Придется начать с самого начала — разобраться с процессом преобразования исходного HTML в содержимое страницы, который называют Critical Rendering Path (критический путь рендеринга).
Построение дерева DOM
Получив с сервера документ, парсер браузера каждый тег превращает в узел и строит их иерархию
<html>
<head>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Давай построим DOM</h1>
</body>
</html>
В результате получается дерево узлов (node), или просто DOM-дерево, в котором вложенные элементы представлены в виде дочерних узлов с полным набор атрибутов:
html
head
link rel="stylesheet"
href="style.css"
body
h1 Давай построим DOM
Исторически-лирическое отступление. Когда все только задумывалось, страницы рендерились из статики, каналы связи были неторопливыми, а пользователи — непритязательными. Документ мог грузиться довольно долго и практически не изменялся за время жизни в браузере. Для того, чтобы страница как можно скорее отвечала на действие пользователя, любое изменение DOM автоматически запускало процесс повторного рендеринга страницы.
Но ведь добавление очередного узла в DOM в процессе парсинга — это тоже изменение? Так и есть. Стоит узлу попасть в объектную модель документа — страница перерисовывается. До сих пор! На практике это означает, что документ отрисовывается по частям, браузер даже не дожидается окончания загрузки. Пользователи интернета со стажем помнят, как это выглядело. Ну а сегодня можно в инструментах разработчика поставить качество связи Slow 3G на вкладке network и насладиться загрузкой какой-либо статической страницы.
Нам как разработчикам важен сам факт — любое изменение физического DOM требует перерисовки страницы, а значит — времени и ресурсов.
Построение CSSOM-дерева
Хорошо, верстку мы получили, надо сделать ее красивой. Пробежимся по полученному ранее DOM и добавим каждому узлу соответствующие стили. Говоря языком документации — сформируем CSS object model или CSSOM.
Если в примере выше в файле style.css будет
body { font-size: 16px; }
h1 { font-size: 20px; }
то соответствующее CSSOM дерево будет выглядеть следующим образом:
html
head
link rel="stylesheet"
href="style.css"
body font-size: 16px
h1 font-size: 20px
Давай построим DOM
Еще одно отступление. CSS недаром называют Cascading Style Sheets — каскадными таблицами стилей. Чтобы применить стиль для конкретного узла, необходимо его посчитать, разобрав полностью всю таблицу стилей документа. Следовательно — на это время процесс рендера нужно приостановить.
CSS является блокирующим ресурсом. Причем не только для верстки, но и для скриптов, которые также вынуждены дожидаться построения CSSOM для того, чтобы начать выполняться.
На практике это означает, что мы должны учитывать время, которое нужно для построения CSSOM при написании своего кода.
Хорошая новость — CSS блокирует рендер только при применении. Стили, указанные с помощью медиа-атрибута для мобильного разрешения, не являются блокирующими (и не разбираются) при рендере десктопной версии и наоборот, а стили для ландшафтной ориентации устройства не участвуют в рендере для портретной.
Плохая новость — изменение размера окна браузера или поворот мобильного из одной ориентацию в другую может вызвать срабатывание медиа-атрибута и блокировку рендера страницы для пересчета CSS. Если забыть про этот нюанс, можно потерять немало времени на поиск ошибок не в том месте.
Запуск JavaScript
Ура, добрались! JavaScript — это наша альфа и омега, именно с его помощью мы делаем веб-страницы интерактивными — такими, к каким привык современный пользователь. Но за счет чего мы это делаем? За счет изменения DOM и стилей.
Стоп, скажете вы! И будете правы — именно стоп. Когда парсер доходит до тега <script>, разбор документа блокируется до тех пор, пока скрипт не будет полностью прочитан и выполнен.
Поскольку JS требуется ссылаться на какой-либо существующий элемент, код скрипта или ссылка на JS-файл должны располагаться в документе после объявления соответствующего элемента.
Если скрипт требуется разместить в начале документа (например, код счетчика систем аналитики) — можно загрузить его асинхронно, указав атрибут async. Это позволит не блокировать рендер страницы. При этом надо понимать, что выполнение самого кода начнется только после окончательной загрузки скрипта. Если же код необходимо запустить только после того, как документ будет полностью разобран, можно использовать атрибут defer
Создание Render-дерева
А как быть со служебной информацией, например, с метаданными — их-то показывать пользователю не требуется, а в DOM они присутствуют? Давайте скроем «ненужное» и соберем для рендера дерево только из тех элементов, что должны быть видны на странице! Заодно избавимся от узлов, спрятанных CSS, например, через правило display: none.
Полученный результат так и назовем: Render-дерево. То есть совокупность DOM и CSSOM, включающая в себя только видимый контент — то, что в конечном итоге будет отображено на странице.
html
body font-size: 16px
h1 font-size: 20px
Давай построим DOM
Генерация раскладки
Кажется, все готово? Почти! Осталось разобраться с масштабом, а точнее с размером видимой области документа (viewport). Это нужно для того, чтобы рассчитать стили, заданные в относительных величинах, таких как проценты или единицы vh и vw.
<meta name="viewport"
content="width=device-width,initial-scale=1">
Код выше устанавливает ширину видимой области в соответствии с шириной устройства, что используется в большинстве случаев. Если тег явно не указан, то используется стандартное значение в 980 пикселей.
Отрисовка
Вот теперь точно все. И к этому моменту мы уже понимаем, что влияет на скорость рендера страницы — размер DOM и сложность примененных стилей.
Первый этаж. Что же такое DOM
В принципе, все вышеизложенное есть в документации. Но теперь, когда мы освежили воспоминания, проще будет разобраться с дальнейшим.
Для начала, давайте разделим DOM и HTML. Несмотря на то, что первый строится на базе второго, они вовсе не идентичны. Причин две: человеческий фактор и JavaScript.
Первое — скорее «защита от дурака». Если верстальщик ошибется или забудет закрыть какой-то тег, браузер постарается все исправить. Не из альтруизма, конечно — ему важно, чтобы модель данных оставалась корректной.
<!doctype html>
<html>
<h1>Давай построим DOM
</html>
Верстка выше не содержит обязательные для HTML теги <head> и <body>, кроме того, мы «забыли» закрыть <h1>. Но если мы посмотрим в инструменты разработчика, то увидим что браузер все исправил.
<html>
<head></head>
<body>
<h1>Давай построим DOM</h1>
</body>
</html>
html
head
body
h1 Давай построим DOM
Второй вариант, когда DOM отличается от исходного документа, — гораздо ближе к теме современной веб-разработки. Чем занимается любой фронтенд-фреймворк? Изменяет DOM!
Так что же такое DOM? Во-первых, это программный интерфейс доступа к элементам HTML-документа. Он представляет собой валидную модель данных (исправлены ошибки), которая может быть изменена с помощью JavaScript (в современных реалиях скорее не «может быть», а просто изменена).
Еще одно исторически-лирическое отступление. Когда компьютеры были большими, а веб маленьким, мало кто мог представить, что писать код для одной страницы будут десятки людей из разных стран. Про встраивание сторонних модулей не слышали, виджетов не видели. Древние, в общем, разработчики были. И мудрые. По-своему. Сделали глобальную область видимости для всего браузера — им так было удобнее.
Плюсами такого решения мы пользуемся до сих пор, с легкостью добираясь до любого элемента html-документа вне зависимости от того, насколько глубоко он вложен. Не менее полезна и возможность задавать или править глобально стили, например, для нормализации значений по умолчанию всего документа или задания дефолтного поведения
* { box-sizing: border-box }
Но за все приходится платить. Отсутствие возможностей изоляции в DOM вынуждает «костылить» префиксы в именах классов или придумывать многоэтажные системы именования, чтобы избежать пересечений. Все это в конечном итоге увеличивает и размеры файлов, и сложность работы с ними. А значит, заставляет искать другое решение.
Что за дом без веранды? Пристроим Shadow DOM
Для того, чтобы избавиться от глобальной области видимости, есть всего два принципиальных решения: создать «документ внутри документа», или «DOM внутри DOM».
Примечание: грустную историю о болях корректного встраивания нескольких iframe на странице не пропустила самоцензура, так что сразу перейдем к варианту номер два.
Знакомьтесь: Shadow DOM — изолированное дерево со своими элементами и стилями, которые не зависят от исходного документа.
Изначально оно потребовалось самим браузерам для того, чтобы спрятать «начинку» UI-примитивов — кнопок, инпутов, селекторов и так далее. Но как только этот инструмент попал в открытый доступ, то есть стал частью спецификации, его оценили и простые веб-разработчики. Хотя бы как удобный способ создания собственных универсальных элементов.
<html>
<head></head>
<body>
<my-component></my-component>
</body>
</html>
Все, что для этого нужно, — задать в верстке элемент-контейнер — произвольный тег (можно воспользоваться стандартным div, а можно задать собственное имя), найти его и прикрепить теневое дерево.
const myComponent = document.querySelector("my-component");
const shadow = myComponent.attachShadow({mode: 'open'});
Страница по-прежнему пуста, но в инспекторе мы видим, что у <my-component> появился #shadow-root. Добавим в теневое дерево верстку
const mainWindow = document.createElement("main-window");
mainWindow.innerHTML = "<p>Play</p>";
shadow.appendChild(mainWindow)
и стили
const styles = document.createElement("style");
styles.textContent = `
main-window {
display: flex;
width: 200px;
height: 100px;
background: beige;
border-radius: 10px;
box-shadow: 0px 6px 10px 0px black;
justify-content: center;
align-items: center;
}
`;
shadow.appendChild(styles)
Благодаря созданию отдельного теневого дерева мы обеспечили инкапсуляцию: стили, применяемые внутри Shadow DOM, экранированы от внешней среды, а для методов DOM API создается отдельный контекст.
Что это дает? Возможность использовать одинаковые ID в разных контекстах — мы можем достучаться методами API до нужного нам элемента, но при этом он невидим снаружи для других контекстов.
Легко заметить, что теперь у любого элемента в теории может быть 2 типа поддеревьев DOM: Light tree — обычное DOM-поддерево, состоящее из HTML-потомков, и Shadow tree. Какое из них победит?
Если у элемента имеются оба поддерева, браузер по умолчанию отрисовывает только теневое. Но главная прелесть в том, что служебный тег <slot> позволяет комбинировать Light и Shadow tree. Он дает контроль над размещением непосредственных потомков сложного DOM-элемента в нужном месте и обеспечивает композицию. А следовательно — Shadow DOM можно использовать в качестве шаблона для layout-элементов.
<script>
customElements.define('product-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<div>Товар:
<slot name="product"></slot>
</div>
<div>Цена:
<slot name="price"></slot>
</div>
`;
}
});
</script>
<product-card>
<span slot="product">Велосипед</span>
<span slot="price">12000 руб.</span>
</product-card>
<product-card>
<span slot="product">Самокат</span>
<span slot="price">2000 руб.</span>
</product-card>
В этом примере браузер берет элементы из обычного DOM и отображает их в соответствующих слотах теневого дерева.
Это открывает широкие возможности для создания собственных компонентов. Но как быть, когда хочется расширить возможности стандартных элементов?
Одна из проблем, с которыми приходится сталкиваться при динамическом добавлении CSS-свойств элементу через element.style — невозможно напрямую использовать медиазапросы или определить псевдоклассы и псевдоэлементы. Shadow DOM дает доступ к элементу контейнеру через селектор ::host
let myElement = document.querySelector('div');
myElement.attachShadow({
mode: 'open',
});
myElement.shadowRoot.innerHTML = `
<style>
:host(:hover) {
color: blue;
}
</style>
<slot></slot>
`;
Мы «подцепили» условному div-у реакцию на наведение мыши, при этом не создавали новые классы и не изменяли внешние стили.
Остается отметить, что теневых DOM на странице может быть неограниченное количество, а каждому корню теневого DOM должен соответствовать элемент в обычном DOM, который будет являться базовым хостом.
Ограничения Shadow DOM:
создается только с помощью JS, а потому не существует возможности предварительного рендера (SSR). Данное ограничение можно обойти, но это тема для другой статьи
Требует внешнего контроля жизненного цикла компонентов и их инициализации во внешней среде
Из-за использования сайтами политик безопасности, существуют серьезные ограничения на добавление стилей к элементам внутри теневого DOM. Дело в том, что CSP (Content Security Policy) запрещает парсить стили из строки. Обойти это можно отключением политики безопасности, но при разработке виджета для стороннего сайта это, пожалуй, самое плохое решение. Более универсальные решения — создание динамических стилей через element.style или добавление в Shadow DOM внешнего css-файла
создание Shadow DOM, как и любого другое действие, требует выделения дополнительных ресурсов, а потому злоупотреблять им не стоит
отсутствует поддержка в IE
Беремся за второй этаж. Зачем понадобился Virtual DOM?
На самом деле ответ прост и мы его уже знаем — физический DOM устроен так, что любое его изменение автоматически вызывает перерисовку страницы, а это не всегда нужно.
Представьте, что вам нужно выводить на странице список товаров
<!doctype html>
<html lang="en">
<head></head>
<body>
<ul class="product">
<li class="product__item">Товар</li>
</ul>
</body>
</html>
DOM для такой верстки:
html
head lang="en"
body
ul class="product"
li class="product__item"
"Товар"
А теперь добавим в список самокат, а дефолтный товар заменим на велосипед. Для этого надо найти в DOM список, создать новый элемент, добавить в него контент, обновить контент в старом элементе списка, после чего обновить сам DOM
const productItemOne = document.getElementsByClassName("product__item")[0];
productItemOne.textContent = "Велосипед";
const productItemTwo = document.createElement("li");
productItemTwo.classList.add("product__item");
productItemTwo.textContent = "Самокат";
const product = document.getElementsByClassName("product")[0];
product.appendChild(productItemTwo);
При этом каждый раз, когда мы «дергаем» DOM API — запускается алгоритм пересчета изменений и рендера страницы.
Откровенно говоря, проще заменить старый именованный список на новый
const product = document.getElementsByClassName("product")[0];
product.innerHTML = `
<li class="product__item">Велосипед</li>
<li class="product__item">Самокат</li>
`;
В этом варианте мы выполнили всего одно обращение к DOM и единожды отрисовали страницу заново. А значит — выиграли в производительности.
Хочу такой же, но с перламутровыми пуговицами!
Как этого добиться? Сделать «копию» DOM, выполнить все нужные преобразования, и только когда все посчитано — обновить реальный DOM. Поздравляю, мы только что сформулировали основную идею Virtual DOM.
Давайте сформулируем и реализацию. В качестве копии — воспользуемся самым обычным JS-объектом. Для нашего примера со списком продуктов его можно представить как
const vdom = {
tagName: "html",
children: [
{ tagName: "head" },
{
tagName: "body",
children: [
{
tagName: "ul",
attributes: { "class": "product" },
children: [
{
tagName: "li",
attributes: { "class": "product__item" },
textContent: "Товар"
}
]
}
]
}
]
}
Первый плюс мы уже получили — нет нужды лишний раз перерисовывать страницу. А значит, уже выиграли в производительности.
Второй плюс вытекает из самого факта того, что мы работаем с объектом. Мы избавились от необходимости постоянно обращаться к громоздкому браузерному API. Ставим еще плюсик в производительность.
Но можно ли еще как-то усовершенствовать идею? Конечно! в DOM у нас складывается весь документ целиком, и его копия — довольно большой объект. Из которого нам в нашем случае нужен всего лишь один компонент!
Давайте сделаем следующий шаг: создадим для каждого компонента свой объект и будем работать с ними, как с некими разделами Virtual DOM.
const product = {
tagName: "ul",
attributes: { "class": "product" },
children: [
{
tagName: "li",
attributes: { "class": "product__item" },
textContent: "Товар"
}
]
};
Мы еще на порядок упростили работу с конкретным компонентом и вновь выиграли в производительности, поскольку взаимодействуем с небольшим объектом, а не с копией всего DOM в целом.
Как работает виртуальный DOM
Вернемся к нашему примеру и проделаем те же операции. Поскольку API браузера нас больше не ограничивает, мы просто создадим новый объект
const copy = {
tagName: "ul",
attributes: { "class": "product" },
children: [
{
tagName: "li",
attributes: { "class": "product__item" },
textContent: "Велосипед"
},
{
tagName: "li",
attributes: { "class": "product__item" },
textContent: "Самокат"
}
]
};
и сравним исходный product
и новый copy
, выделив изменения.
const diffs = [
{
newNode: { /* textContent: "Велосипед" */ },
oldNode: { /* textContent: "Товар" */ },
index: /* index of element in parent's nodes */
},
{
newNode: { /* textContent: "Самокат" */ },
index: { /* */ }
}
]
Остается пройтись циклом по диффам, обновить старые или добавить новые элементы
const domElement = document.getElementsByClassName("product")[0];
diffs.forEach((diff) => {
const newElement = document.createElement(diff.newNode.tagName);
if (diff.oldNode) {
domElement.replaceChild(diff.newNode, diff.index);
} else {
domElement.appendChild(diff.newNode);
}
})
Создать новый объект, посчитать его разницу с предыдущим и точечно обновить элементы — намного быстрее, чем добавить на страницу новую строку с версткой, вызвав тем самым ее полный пересчет и перерисовку.
Давайте подведем промежуточный итог. Мы сформулировали принципиальную идею отказаться от работы непосредственно с DOM, пришли к тезису что удобнее работать с отдельным объектом для каждого компонента и предусмотрели механизм, который позволяет запустить рендер страницы. Разумеется, это не production версия Virtual DOM, а только объяснение принципа его работы. В зависимости от того, какой фреймворк вы используете (и даже от его версии) — детали реализации могут отличаться.
Virtual DOM — это объектное представления JavaScript, которое позволяет взаимодействовать с элементами DOM более простым и производительным способом. Объект-представление можно изменять так часто, как это необходимо, после завершения вычислений вызывается механизм сравнения изменений. В реальный DOM вносятся только финальные изменения, что происходит намного реже, не требует большого количества обращений к API браузера и, следовательно, повышает производительность.
Для тех, кто готов достроить третий этаж
Дошедшим до этого раздела уже должно быть понятно, почему современному фронтенд-разработчику не стоит пытаться «согнуть ложку», а точнее — пытаться общаться с DOM напрямую. Для этого есть более производительный и удобный Virtual DOM, предоставляемый фреймворками.
Но постойте, браузер-то умеет работать только со своим API, а наши компоненты — только с Virtual DOM. Нужен движок, «переводчик» пожеланий компонентов в инструкции, понятные браузеру. И этот движок мы должны загрузить в браузер вместе с самими компонентами.
Как уменьшить размер бандла?
Проблема в том, что на этапе компиляции мы не знаем, какие инструкции на самом деле нужны, а какие нет, а потому приходится загружать все.
Это не так страшно, если приложение не очень большое, а ресурсов компьютера достаточно. Но когда речь заходит о мобильных устройствах, размер бандла и требуемые объемы памяти становятся критически важными параметрами.
Давайте предложим компонентам обращаться не к движку рендеринга, а непосредственно к инструкциям. Пусть каждый компонент заранее определится, какие ему нужны, тогда на этапе компиляции приложения можно будет отсеять невостребованные.
Таким образом решается задача tree-shakable — уменьшения размера сборки, которая загружается в браузер за счет удаления неиспользуемых фрагментов кода.
Поговорим о потреблении памяти
Давайте еще раз посмотрим, как Virtual DOM осуществляет рендеринг. На первом шаге мы на базе реального DOM строим виртуальное дерево, следовательно — для каждого элемента нам нужно выделить какой-то объем памяти. Когда приходит время отрисовать новое состояние, мы строим новое виртуальное дерево, а значит — для каждого элемента повторно выделяется нужный объем памяти. Далее старое и новое виртуальные деревья сравниваются, и если есть разница — изменения применяются к физическому DOM.
Получается, что на каждом этапе рендера нам требуется повторно выделять память в размере, необходимом для того, чтобы отрисовать дерево целиком.
И снова — повод задуматься. Если у нас на какой-то момент времени есть существующее актуальное состояние DOM уже отрисованное браузером, и изменяется только один элемент — зачем нам строить новое виртуальное дерево целиком? Что, если можно было бы пробежаться по старому и взять из него для построения нового те элементы, которые не изменились? Тогда нам потребуется дополнительно выделять память только в случае создания нового узла или изменения старого.
Этот алгоритм реализует небольшая (всего 2,6 КБ) библиотека под названием Incremental DOM.
Здравый вопрос — почему же тогда этот вариант до сих пор используется не всеми? Из «большой тройки» фреймворков, Incremental DOM применяется только в Angular, а React и Vue предпочитают старый добрый Virtual DOM.
Все дело в том, что Angular — единственный фреймворк большой тройки, изначально построенный на архитектуре с использованием template, когда все компоненты пишутся с использованием шаблонов. Посмотрим на пример функции renderPart() из документации библиотеки Incremental DOM
function renderPart() {
elementOpen('div');
text('Hello world');
elementClose('div');
}
и мысленно подставим на место elementOpen() и elementClose() — selector компонента Angular, а на место text() — template.
function renderPart() {
elementOpen('some-component');
text(`
… some template html
`);
elementClose('some-component');
}
Для тех, кто добрался до финала
Пришло время подводить итоги и разобраться с многоточием, которое было поставлено во вступлении.
Не пытайся изменить DOM… без причины. Первое, что нужно понять: DOM существует. Но его задача — обеспечить отрисовку страницы, заранее подготовленной с помощью других инструментов. Таких, как Shadow, Virtual и Incremental DOM. И современный разработчик должен знать, в чем они схожи, чем отличаются и как выбрать наиболее подходящее решение для конкретной задачи.
Если есть чем дополнить — комментарии можно оставлять под текстом или мне в телеграм.
Другие наши статьи:
Комментарии (24)
elektroschwein
28.09.2021 09:48+3Мда, столько костылей и библиотек, только для того, чтобы обойти суровую особенность DOM перерендивать все при малейшем изменении...
Не проще было бы добавить в DOM API какие-нибудь методы, временно отключающие перерисовку содержимого страницы и включающие ее обратно, вызывать один в начале, потому поменять все нужные свойства, удалить-добпаить элементы, а потом сказать браузеру, типа все готово, можешь рендерить?
OldVitus
28.09.2021 10:47+2Мда, столько костылей и библиотек, только для того, чтобы обойти суровую особенность DOM
которой нет
elektroschwein
28.09.2021 11:01А она есть. Откройте встроенный хромовский профилировщик и посмотрите, сколько всего там делается при банальном добавлении одной ноды в дерево.
OldVitus
28.09.2021 11:29+3Хотите сказать что Virtual DOM чудесным образом уменьшает это все при добавлении одной ноды в дерево?
Я открывал профилировщик и сравнивал React с одной мало известной библиотекой - результат время на рендеринг без изменений, процессор и память в разы больше утилизируются при использовании Virtual DOM
faiwer
28.09.2021 14:04+1Хотите сказать что Virtual DOM чудесным образом уменьшает это все при добавлении одной ноды в дерево?
Когда говорят что virtual-dom очень быстрый, то обычно сравнивают какое-нибудь точечное изменение в DOM, с заменой какого-нибудь большого узла целиком. Аргументируют это тем. что за счёт vDom и реконсиляции библиотека знает что поменять, а ваше %frameworkname% нет и патчит всё по хардкору.
И откровенно говоря, в те времена когда появился React это было более-менее правдиво. Много SPA тех времён и правда нещадно экплуатировали
innerHTML
, потому что это было просто. Прогнал новые данные по функции-шаблону, получил кусок HTML, заменил старый на новый. По пути потерял весь state вроде позиции скролла, курсор, :focus и пр…Сейчас это уже совсем не так, и все популярные решения стараются бить по DOM как можно более точечно. Например, если используется observable, то можно сделать автоматически отдельную подписку на нужные значения, от которых зависит какой-нибудь attribute. Ну и разница выходит в BigO, который очень разный. Где-то идёт зависимость от числа задействованных реактивных элементов, а где-то от числа нод на сравнение с прошлым слепком.
elektroschwein
28.09.2021 14:17Это если одну ноду добавляем. А если одну, в нее другую, потом ещё у какой-то вложенной стиль поменять, у другой вложенной текст, или ещё что -- то в одном случае оно перерендиреваться будет каждый раз, а в случае с VDOM только единожды.
nin-jin
28.09.2021 15:40+3Не будет оно каждый раз перерендериваться. Браузеры не совсем дураки разрабатывают.
Попробуйте в Реакте перенести жирный кусок VDOM из одного родителя в другой - он перерендерит всё с нуля вместо 1 операции с DOM.
faiwer
28.09.2021 16:13Вот интересно. А есть решения где п2 как-то иначе сработает? Имею ввиду автоматику, не вручную. Наверное для этого нужна некая глобальная идентификация таких звеньев. $mol?
OldVitus
28.09.2021 10:58+13Virtual DOM был придуман не для оптимизации перерисовки, а для того чтобы натянуть сову на глобус - заменить innerHTML и продолжать генерить страницы как привыкли.
Maxim_from_HW Автор
28.09.2021 14:19А я могу поставить плюс и поставлю????, только немного переформулирую — не только для оптимизации, но и для сохранения привычных абстракций (как сказали выше в комментах). Одно другому не противоречит.
nin-jin
28.09.2021 13:02Angular — единственный фреймворк большой тройки, изначально построенный на архитектуре с использованием template, когда все компоненты пишутся с использованием шаблонов
Я бы сказал, что React (с его клонами) - чуть ли не единственный, где не "используются template".
i360u
Браузер не инициирует лишнюю перерисовку страницы если обращения к DOM API происходят в коде синхронно. "Точечные изменения", которые делают за вас библиотеки - именно так и работают. Из этого следует, что взаимодействовать с DOM напрямую - это вовсе не плохая идея, а напротив, путь к более эффективным решениям. Подтверждением этому является множество современных библиотек, отказавшихся от концепции Virtual DOM. В общем, неплохая статья, где-то до середины.
JustDont
Дьявол кроется в деталях — действительно на экране ничего не изменится. А вот вызвать reflow (который может быть весьма вычислительно тяжелым, в зависимости от DOM) в синхронном коде — не особо сложно даже в современнейших браузерах (которые сами по себе сильно оптимизируют reflow/repaint, когда могут).
i360u
Как? Приведите пример.
Alexandroppolus
Поменять что-то в элементе и тут же спросить у него, например, offsetHeight. Вроде ещё недавно такое приводило к рефлоу, хотя может быть сейчас оптимизировали и это.
i360u
Хороший пример. Показывает, что получение результатов вычисления требует этих самых вычислений, независимо от степени оптимизации. Ну, то есть, если специально запрашивать рефлоу и он произойдет - то это ожидаемое поведение. Я же говорю о ЛИШНИХ перерисовках, которые никак не оптимизируются на уровне синхронизации с Virtual DOM, потому как при этой синхронизации используется тот-же самый синхронный вызов методов DOM API.
nin-jin
Тут есть такой момент - меняем мы один элемент, а потом спрашиваем что-то от другого. При этом часто нам нужно именно прошлое значение свойства, а не пересчитанное с учётом изменения другого элемента. Грубо говоря, в идеале рендер должен происходить так: сначала прочитали из дома всё, что нам нужно, потом обновили дом с учётом прочитанного. Но это не масштабируется. Когда мы разбиваем на компоненты, каждый из них начинает читать и писать вперемешку, что приводит к лишним пересчётам и неожиданным результатам. Тут бы иметь какую-то форму версионирования или откладывания фактических изменений до завершения рендера.VDOM с эти конечно худо-бедно справляется, но цена за это непомерная. IDOM в этом свете выглядит гораздо лучше.
victor-homyakov
Библиотека https://github.com/wilsonpage/fastdom в принципе позволяет такое сделать и при разбиении на компоненты: все вызовы чтения/изменения DOM в компонентах надо делать через
fastdom.measure()
иfastdom.mutate()
, а библиотека сама их сгруппирует.JustDont
Ну так это всё никак не регулируется спецификациями — это на самом деле чисто browser-specific поведение, что, например, тот же хром не будет делать лишних движений, если может обойтись без reflow. А какой-нибудь старый или просто местячковый браузер — вполне может.
Но, разумеется, общего неправильного посыла статьи это не меняет, потому что как ниже правильно заметили, vdom — он про сохранение привычных абстракций, а не про оптимизацию быстродействия. В конце концов, никто за десятилетия не выкатил разгромных бенчмарков, в которых реакт бы рвал всех по производительности. Совсем наоборот, когда ты ангулярщикам рассказываешь про то, что в реакте лишний раз render() страшно дернуть (много возни с vdom и легко получить брутальный performance hit на ровном месте в некоторых не особо экзотичных обстоятельствах), они на тебя смотрят немного с жалостью.
nin-jin
А потом Ангулящикам показываешь, как одно пролетающее в стороне событие вызывает пересчёт всех компонент приложения, и они уже с грустью смотрят на необходимость переводить все компоненты с автоматического трекинга изменений на ручной.
Maxim_from_HW Автор
Про сохранение абстракций — хорошая формулировка, согласен (если добавить к ней «в том числе и...»). Но общий посыл не о том, что VDOM позволяет оптимизировать быстродействие, и не про сам VDOM как таковой. А про то, как вообще с изменением подходов и задач развивалось взаимодействие с DOM. Как решения, дававшие плюсы при одном подходе, оказывались минусами при другом, и это приводило к следующей итерации. Как и в любой статье — что-то упрощено, да. Это неизбежно, ведь и весь путь изменений в работе с DOM не был «предначертан заранее», это результат многих итераций, чтобы получить искомое. Текущее состояние тоже не финал, что-то да придет на смену.
Сравнение на примере точечного изменения тоже не совсем корректно, да и сейчас, как совершенно верно замечено, подход меняется.
gearbox
https://gist.github.com/paulirish/5d52fb081b3570c81e3a