Назад

ReactJavaScriptTypeScript

Обновление DOM в React (React-reconciliation)

Оптимизация в React, одна из основных задач, которая заключается в максимально эффективной отрисовке DOM элементов, при их изменении.

Image

React reconciliation (свертка) - алгоритм, используемый для обновления виртуального DOM и определения какие части нужно заменить, а какие оставить без изменения.

Тут, я думаю, стоит начать с того, почему вообще стоит такая задача? Почему бы сразу не перерисовать всё DOM дерево? Дело в том, что работа с DOM деревом является очень трудозатратной операцией и React решает эту проблему с помощью виртуального DOM, которое является легковесной копией реального DOM.

Погружаясь в алгоритмы, можно понять, что базовый алгоритм обработки и рендера DOM элементов составляет O(n^3). Говоря простым языком, то в React для отображения 100 элементов, потребовалось бы около 1 000 000 операций сравнения. Думаю об эффективности такого алгоритма говорить не стоит... НО React решает эту проблему следующим образом.

Команда React решила использовать эвристический алгоритм, сложность которого O(n) и основанного на двух предположениях:

  1. Из двух элементов разных типов получатся разные деревья.
  2. Разработчик может указать, какие дочерние элементы могут оставаться неизменными при разных рендерингах, с помощью key свойства.

Эвристический алгоритм не имеет как таковой конкретной реализации, это название для любого алгоритма, оптимальность которого не гарантирована, но оно достаточно хорошее для выполнения поставленных задач. Переводя это в область React и рендера DOM элементов, можно сказать что в большинстве случаев React будет модифицировать DOM дерево оптимальным образом, но гарантировать, что для любой отрисовки это будет выполнено максимально эффективно мы не можем (то есть сложность может быть и больше чем O(n)).

Теперь чуть более подробнее о том, как React определяет как ему эффективно отрисовать элементы в DOM.

Элементы различных типов

Для примера, у нас есть следующий JSX-код:

<div class="wrapper">
  <InfoBlock />
</div>

Допустим, у нас после некоторых манипуляций тег <div class="wrapper">, в котором у нас находится компонент <InfoBlock />, превратился в <span class="wrapper">

В данном случае это приведет к полной перерисовке тега <div class="wrapper"> (что не удивительно), но и также всех вложенных элементов в него (в нашем случае это компонент <InfoBlock />). 

Теперь рассмотрим другой вариант развития событий

Элементы одного типа

В качестве примера оставим предыдущий:

<div class="wrapper">
  <InfoBlock />
</div>

Теперь мы просто хотим добавить новый атрибут id к тегу <div class="wrapper" >, в итоге у нас получится следующее:

<div class="wrapper" id="info-badge">
  <InfoBlock />
</div>

Как сейчас поведет себя React?

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

Списки и ключи

Финальный раздел, в котором поговорим о том, как React обрабатывает списки и для чего же нужен это преслову́тый key.

Рассмотрим список <ul>:

<ul class"list">
  <li>Яблоко</li>
  <li>Груша</li>
  <li>Банан</li>
</ul>

Предположим, что мы хотим добавить элемент <li>Апельсин</li>, в конец списка. В данном случае поведение React ожидаемо, он сравнит все элементы начинаю с первого и в конец добавить новый. 

Проблемы начинаются с момента, когда мы пытаемся вставить новый элемент в начало списка или в середину. Для примера рассмотрим случай вставки в начало списка. React опять начинает сравнивать всем элементы друг с другом, и поскольку старый список уже не совпадает с новым, React начинает перерисовывать все элементы списка, как бы сдвигая их вниз.

Такое поведение является неэффективным, так как вместо того чтобы просто вставить новый элемент в начало, React начинает перерисовывать все элементы заново, а как мы уже поняли - любые взаимодействия с DOM являются трудозатратными. Эту проблему и призван решать атрибут key.

Если у дочерних элементов есть ключи, React использует их для сопоставления дочерних элементов в исходном дереве с дочерними элементами в последующем дереве. 

Ключи должны быть стабильными, предсказуемыми и уникальными. Нестабильные ключи (например, созданные с помощью Math.random()) приводят к ненужному повторному созданию множества экземпляров компонентов и узлов DOM, что может привести к снижению производительности и потере состояния дочерних компонентов.

Доп. информация

Reconciliation в React делится на две основные фазы:

1. Фаза Render Phase (Diffing) — вычисление изменений

Здесь создается новое Work-In-Progress (WIP) Fiber-дерево, которое используется для вычисления изменений. 

2. Фаза Commit Phase (Patching) - обновление DOM

В этой фазе изменения, вычисленные в предыдущей фазе, применяются к реальному DOM.

По всем вопросам, замечаниям и дополнениям, вы можете написать мне в TG - @ngusev_dev