React — это JavaScript библиотека для создания пользовательских интерфейсов от Facebook. Она была разработана «с нуля», с упором на производительность. В этой статье я расскажу вам о diff-алгоритме и механизме рендеринга, который использует React, что позволит вам оптимизировать ваши приложения.
Diff Алгоритм
Перед тем как мы углубимся в детали реализации, довольно важно, чтобы вы имели представление о том, как работает React.
var MyComponent = React.createClass({
render: function() {
if (this.props.first) {
return <div className="first"><span>A Span</span></div>;
} else {
return <div className="second"><p>A Paragraph</p></div>;
}
}
});
В любой момент времени вы можете описать, как будет выглядеть ваш UI. Важно понимать, что результат рендеринга не является фактическим DOM деревом. Это всего лишь легковесные JS объекты, которые мы называем «виртуальный DOM».
React будет использовать образ вашего виртуального DOMа, чтобы найти минимальное количество шагов, которые позволят перейти от предыдущего состояния отображения до следующего. Например, если мы вставляем <MyComponent first={true} />
, потом заменяем его <MyComponent first={false} />
, а потом удаляем его, то инструкции для DOM будут выглядеть следующим образом:
Исходное состояние к первому
- Создать узел:
<div className="first"><span>A Span</span></div>
Первое ко второму
- Заменить атрибут:
className="first"
наclassName="second"
- Заменяем узел:
<span>A Span</span>
на<p>A Paragraph</p>
Второе к конечному
- Удалить узел:
<div className="second"><p>A Paragraph</p></div>
Уровень за уровнем
Нахождение минимального количества модификаций между двумя произвольными деревьями — задача O(n^3). Как вы могли догадаться, это неособо подходит для наших задач, поэтому React использует простой и весьма эффективный эврестический метод для нахождения аппроксимации, которая позволяет добиться сложности алгоритма, приближенной к O(n).
React просто сравнивает деревья по внутренним узлам. Это радикально меняет сложность алгоритма и не является большой потерей, т.к. в веб-приложениях нам очень редко приходится заниматься «вертикальным»(по разным уровням) перемещением внутреннего узла. Обычно мы перемещаем узлы «горизонтально»(на одном уровне).
Список
Давайте предположим, что у нас есть компонент, который за первую итерацию отрисоввывает 5 компонентов в виде списка, а следующая итерация добавляет один компонент в его середину. Согласитесь, будет весьма сложно найти разницу между двумя результирующими деревьями.
По умолчанию, React ассоциирует первый компонент предыдущего списка с первым компонентом нового списка и т.д. Вы можете установить элементу ключ, дабы предоставить React возможность использовать маппинг. На практике, обычно не составляет труда найти уникальные ключи при «горизонтальном» поиске.
Компоненты
Приложение, использующее React, обычно состоит из большого количества пользовательских компонентов, которые в конце концов превращаются в дерево, которое состоит в основном из div
ов. Эта дополнительная информация будет включена в расчет при работе diff алгоритма, т.к. React будет сравнивать только компоненты одного класса.
Например, если <Header>
будет заменен на <ExampleBlock>
, React просто удалит <Header>
и создаст <ExampleBlock>
. При таком подходе, нам не нужно тратить драгоценное время, сравнивая два компонента, между которыми нет ничего общего.
Делегирование событий
Добавление обработчиков событий к DOM элементам — это всегда чрезвычайно медленно и затратно по памяти
Вместо этого, React реализует популярный подход, который называется «делегация событий». Более того, React идет еще дальше и создает свою событийную модель, совместимую с реализацией в W3C. Это означает, что баги обработки событий в IE8 остаются в прошлом, и все имена событий консистентны между браузерами.
Позвольте мне объяснить как это реализовано. Один обработчик события прикреплен к корню документа. Когда возникает событие, браузер предоставляет нам target
DOM элемент. В порядке распространения события внутри иерархии DOM, React не перебирает дерево «виртуального DOM».
Вместо этого мы используем тот факт, что любой компонент React имеет свой уникальный id, который описывает иерархическое положение объекта в DOM. Мы можем использовать простейшие манипуляции со строками, чтобы получить доступ ко всем id родителей. Путем сохранения обработчиков событий в хэш-таблице, мы обнаружили, что добиваемся большей производительности, нежели прикрепляя их к виртуальному DOM. Ниже приведен пример того, что происходит, когда возникает событие:
// dispatchEvent('click', 'a.b.c', event)
clickCaptureListeners['a'](event);
clickCaptureListeners['a.b'](event);
clickCaptureListeners['a.b.c'](event);
clickBubbleListeners['a.b.c'](event);
clickBubbleListeners['a.b'](event);
clickBubbleListeners['a'](event);
Браузер создает новый экземпляр объекта события для каждого обработчика, что позволяет получить доступ к ссылке на объект события или даже изменять его. Как бы то ни было, это означает большое количество выделенной памяти. React в начале своей работы создает пул таких объектов, и когда происходит обращение к одному из них, он берется именно из этого пула. Это существенно уменьшает количество мусора.
Рендеринг
Группировка
Каждый раз, когда вы вызываете setState
у компонента, React отмечает его, как измененный. В конце выполнения цепочки событий, React находит все измененные компоненты и перерисоввывает их.
Подобная группировка означает, что за всё время выполнения цепочки событий, DOM изменится всего один раз. Такой подход является ключём к созданию производительных приложений, но к сожалению, этого всё ещё чрезвычайно трудно добиться, используя нативный JavaScript. В приложениях, созданных с помощью React, вы получаете это «по умолчанию».
Перерисовка поддеревьев
Когда происходит вызов setState
, компонент перестраивает виртуальный DOM для своих детей. Если вы вызываете setState
из корневого элемента, то произойдет полный ререндер всего React приложения: метод render
будет вызван у всех компонентов, даже если они не были изменены. Это звучит немного пугающе и неэффективно, но на практике это замечательно работает, ведь мы не трогаем настоящий DOM.
Прежде всего, мы говорим об отображении пользовательского интерфейса. Из-за того, что пространство экрана ограничено, вы обычно отображаете
от сотен до тысяч элементов одновременно. Поэтому, JavaScript довольно быстро получил возможность создавать полностью управляемый интерфейс.
Другой важный аспект — это то, когда вы пишете React код, вы обычно не вызываете setState
у корневого элемента приложения, когда что-либо изменяется. Вы вызываете его у компонента, у которого возникает событие изменения состояния, или на пару узлов выше. Таким образом, вы почти всегда избегаете вызова изменений у корневого узла. Это означает, что изменения локализованы в тех компонентах, где они происходят.
Выборочная перерисовка поддеревьев
Иногда у вас есть возможность предотвратить перерисовку некоторых поддеревьев — если вы реализуете следующий метод в своем компоненте:
boolean shouldComponentUpdate(object nextProps, object nextState)
Основываясь на разнице предыдущего и последующего состояния компонента, вы можете сказать React, что компонент не нуждается в перерисовке. Используя данное свойство, вы можете добиться огромных улучшений в производительности.
Перед тем как использовать его, вам необходимо сравнить два JavaScript объекта. Возникает много вопросов, например должно ли сравнение быть поверхностным или глубоким? И, если оно глубокое, должны ли мы использовать неизменяемую структуру данных или стоит использовать глубокие копии?
И вы бы так же хотели знать, что функция, которая будет вызываться все время, будет выполняться быстрее, чем за время, которое бы потребовалось для повторной отрисовки компонента, даже если оно и не было бы нужно.
Вывод
Технологии, которые делают React быстрым, не новы. Мы все прекрасно знаем, что изменение DOM является дорогостоящей операцией, что стоит группировать операции записи и чтения из него, что делегирование событий быстрее…
Люди продолжают говорить об этом, потому что на практике их довольно тяжело использовать в обычном JavaScript коде. Именно это и выделяет React из всех остальных — все оптимизации работы с DOM сделаны «по умолчанию». Такой подход не дает выстрелить себе в ногу и сделать приложение медленным.
Модель изменения стоимости производительности React очень проста: каждый вызов setState
перерисоввывает все поддерево. Если вы хотите выжать максимальную производительность, вызывайте его как можно реже и используйте shouldComponentUpdate
, чтобы избежать лишних перерисовок больших поддеревьев.
Автор: xamd