Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

  • задайте вопрос нашим менторам. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
  • расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
  • изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.
Об обучении на Хекслете

Производительность

Преждевременная оптимизация - корень всех зол.

Перед тем, как рассуждать о производительности, я настоятельно рекомендую прочитать Optimization.guide.

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

Напомню, что React работает так:

  1. Монтирование (mount) вызывает рендеринг приложения.
  2. Получившийся DOM вставляется в реальный DOM целиком, так как там ещё ничего нет. А виртуальный DOM, в свою очередь, сохраняется внутри React для последующего обновления.
  3. Изменение состояния приводит к вычислению нового виртуального DOM.
  4. Вычисляется разница между старым виртуальным DOM и новым.
  5. Разница применяется к реальному DOM.

React: Согласование

Согласование

Каждый раз, когда происходит изменение в состоянии компонента, запускается механизм, именуемый "согласование" (reconciliation), который вычисляет разницу (дифф) между прошлым состоянием и новым. С алгоритмической точки зрения происходит поиск отличий в двух деревьях. В общем случае алгоритм, выполняющий это вычисление, работает со сложностью O(n3).

Если события генерируются часто, а виртуальное дерево стало большим, то можно начать замечать лаги невооружённым глазом.

Для решения этой проблемы React настоятельно просит для всех элементов списков использовать аттрибут key, который не меняется для конкретного элемента списка. Подобное требование позволяет оптимизировать работу алгоритма, уменьшив сложность до О(n).

Требование проставлять ключи проверяется самим React. Он сам будет выдавать предупреждения в консоли браузера, если увидит, что вы их не используете.

Рендеринг

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

В расширении React Developer Tools есть специальная галочка, нажав на которую можно увидеть те компоненты, которые рендерятся во время событий. Отображается все визуально, то есть после каждого события отрендеренные компоненты подсвечиваются рамочкой.

Легко заметить, что для приложения, в котором ничего специально не делалось, на любое событие будет рендериться вообще всё. Но события, как правило, меняют только небольшую часть DOM. Ввод текста часто вообще не приводит к изменению в DOM.

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

Напомню, что обновление компонентов запускает следующую цепочку функций:

  1. getDerivedStateFromProps()
  2. shouldComponentUpdate()
  3. render()
  4. getSnapshotBeforeUpdate()
  5. componentDidUpdate()

Остановить перерисовку можно благодаря методу shouldComponentUpdate(). Если этот метод вернёт false, то компонент не будет рендериться вообще. А так как мы подразумеваем, что компонент ведёт себя как чистая функция, то достаточно внутри этого метода проверить, что не изменился props и state. Выглядит это примерно так:

shouldComponentUpdate(nextProps, nextState) {
  return !shallowEqual(this.props, nextProps)
    || !shallowEqual(this.state, nextState);
}

Функция shallowEqual() сравнивает только верхний уровень объектов. Иначе эта операция была бы слишком дорогой. Кстати, здесь становится видно, почему нельзя напрямую изменять состояние: this.state.mydata.key = 'value'. Так как объекты сравниваются по ссылкам, то изменение объекта будет показывать, что объект тот же самый, хотя его содержимое поменялось.

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

See the Pen js_react_performance_pure_component by Hexlet (@hexlet) on CodePen.

Если нажимать кнопку, то видно, что корневой компонент перерендеривается, а вложенный нет.

Но не всё так просто. Очень легко незаметно для самого себя сломать работу PureComponent.

Пропсы по умолчанию

Первая засада ожидает нас при неправильной работе со свойствами по умолчанию:

class Table extends React.Component {
  render() {
    const { options } = this.props;
    return (
      <div>
        {this.props.items.map(i =>
          <Cell data={i} options={options || []} />
         )}
       </div>
     );
  }
}

Казалось бы, безобидный код, но вызов [] каждый раз генерирует новый объект (при условии что options равен false). Проверяется это легко: [] === [] будет ложным. То есть данные не поменялись, но <Cell> будет отрисован заново.

Вывод: используйте встроенный механизм для свойств по умолчанию.

Колбеки

class App extends React.PureComponent {
  render() {
    return <MyInput
      onChange={e => this.props.update(e.target.value)} />;
  }
}

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

Библиотека Immutable.js

Ещё один интересный способ решить проблему перерендеринга приложения - использовать персистентные структуры данных, а конкретно библиотеку immutable.js. Это отдельная тема, рассмотрение которой находится за рамками текущего курса.


Дополнительные материалы

  1. Продуманная оптимизация

<span class="translation_missing" title="translation missing: ru.web.courses.lessons.mentors.mentor_avatars">Mentor Avatars</span>

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят менторы из команды Хекслета или другие студенты.

Для полного доступа к курсу нужна профессиональная подписка

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

Получить доступ
115
курсов
892
упражнения
2241
час теории
3196
тестов

Зарегистрироваться

или войти в аккаунт

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 115 курсов, 2000+ часов теории
  • 800 практических заданий в браузере
  • 250 000 студентов

Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг».

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Rambler
Логотип компании Bookmate
Логотип компании Botmother

Есть вопрос или хотите участвовать в обсуждении?

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг».