Оптимизация производительности React + Redux (1): теория

React.js

Подсказки повествования и образцы кода в этой статье взяты изHigh Performance Redux, Спасибо. Причина, по которой я благодарен, заключается в том, что в последнее время я думал о систематической сортировке решений по оптимизации производительности в рамках стека технологий React + Redux, но не смог найти точку входа. В процессе поиска информации эта Презентация дала мне много вдохновения, и многие ее точки зрения острые и совпадают с моими мыслями. Таким образом, эта статья также ссылается на свои подсказки объяснения, чтобы расширить точки знаний, которые я хочу выразить, в свою очередь.

Возможно, вы слышали о многих сторонних решениях по оптимизации, таких какimmutable.js,reselect,react-virtualizedПодождите, мы поговорим подробнее об инструментах в следующей статье. Первое, что нам нужно понять, это почему возникают проблемы с производительностью, и каковы идеи для решения проблем с производительностью. Когда вы поймете все это, вы обнаружите, что многие проблемы с производительностью не нужно решать с помощью сторонних библиотек классов, просто немного внимания при написании кода или небольшая настройка структуры данных могут иметь большое значение.

Производительность не патч, особенность

У каждого свое понимание производительности. Одна из этих точек зрения считает, что на начальном этапе разработки программы не нужно быть осторожным, а размер программы увеличивается, а затем оптимизируется узкое место. Я не согласен с этим пунктом. Производительность не должна быть патчем, который позже будет размещен, а должна быть частью особняка. С первого дня проекта мы должны рассмотреть возможность10x project: т.е. может выполнять 10 тыс. задач и иметь 10-летний срок службы

Сделайте шаг назад и скажите, что даже если вы обнаружите узкое место на более позднем этапе проекта, уровень компании может не дать вам достаточного графика для решения этой проблемы, ведь бизнес-проект по-прежнему является приоритетным (это зависит от насколько "болезненна" эта проблема с производительностью); Делая шаг назад, даже если вам разрешено вести работы по оптимизации, проект после длительного периода итеративной разработки полностью отличается от оригинала: количество модулей огромно, код связь серьезная, особенно проект Redux влияет на все тело, оптимизировать код очень сложно. В этом смысле рассмотрение производительности в продукте с самого начала также является проявлением готовности к будущему, повышая удобство сопровождения кода.

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

Список проблем с производительностью

Фронтенд-фреймворки любят использовать Todo List как учебник для новичков. Мы также берем список в качестве примера здесь. Предположим, вам нужно реализовать список, клики пользователя имеют эффект выделения и все. Что особенного, так это то, что в этом списке 10 тысяч строк, да, вы правильно прочитали, 10 тысяч строк (разве там не говорилось, что мы собираемся сделать 10-кратный проект :p)

Сначала давайте взглянем на основной код,Appкомпоненты иItemКомпоненты составлены, и ключевой код выглядит следующим образом:

function itemsReducer(state = initial_state, action) {
  switch (action.type) {
    case "MARK":
      return state.map(
        item =>
          action.id === item.id ? { ...item, marked: !item.marked } : item
      );
    default:
      return state;
  }
}

class App extends Component {
  render() {
    const { items, markItem } = this.props;
    return (
      <div>
        {items.map(({ id, marked }) => (
          <Item key={id} id={id} marked={marked} onClick={markItem} />
        ))}
      </div>
    );
  }
}

function mapStateToProps(state) {
  return state;
}

const markItem = id => ({ type: "MARK", id });

export default connect(mapStateToProps, { markItem })(App);

Этот важный фрагмент кода воплощает в себе несколько ключевых фактов:

  1. Каждый элемент списка (item) структура данных{ id, marked }
  2. список (items) структура данных представляет собой тип массива:[{id1, marked}, {id2, marked}, {id3, marked}]
  3. AppРендеринг списка выполняется путем обхода (map) массив спискаitemsосуществленный
  4. Когда пользователь нажимает на элемент, щелкнутый элементidПерейти кitemРедуктор, редуктор пересекаетсяitems, сравните по одномуidспособ найти элементы, которые необходимо пометить
  5. Вернуть новый массив после перемаркировки
  6. Новый массив возвращается вApp,Appвизуализировать снова

Если вы не можете собрать воедино приведенный выше фрагмент кода и факты, которые я описал, вы можете найти его на github.полный кодПросмотрите или запустите.

Я считаю, что для такого требования код большинства людей написан таким образом.

Но это имеет низкую производительность, когда приведенный выше код не говорит вам правду. Когда вы пытаетесь нажать на опцию, выделение опции будет задержано как минимум на полсекунды, и пользователь почувствует, что ответ списка замедлен.

Такие значения задержки не являются абсолютными:

  1. Это явление происходит только тогда, когда количество элементов списка велико, скажем, 10 тыс.
  2. в среде разработки (ENV === 'development') будет быстрее, чем запуск в продакшене (ENV === 'production') работает медленнее
  3. Конфигурация ЦП моего персонального ПК составляет 1700x, задержка для разных конфигураций компьютера будет разной.

диагноз

Так в чем проблема? Давайте взглянем на Chrome DevTools (есть много других связанных с React инструментов производительности, которые также дают представление о проблемах производительности, таких как react-addons-perf,why-did-you-update,React Developer Toolsи т.п. Но недочетов более-менее, пользоваться инструментами разработчика в Хроме надежнее всего)

  • Запустите проект локально, откройте браузер Chrome,Добавить в адресную строку для доступа к адресу элементаreact_perfДоступ к странице проекта по суффиксу, например, если адрес моего проекта: http://localhost:3000/, посетите http://localhost:8080/?react_perf. плюсreact_perfЦель суффикса — включить скрытые точки производительности в React.Эти скрытые точки используются для подсчета затрат времени на определенные операции в React.User Timing APIвыполнить
  • Откройте инструменты разработчика Chrome и переключитесь на панель производительности.
  • Нажмите кнопку «Запись» в верхнем левом углу панели производительности, чтобы начать запись информации о производительности.

  • Нажмите на любой элемент в списке
  • Когда выбранный элемент переходит в выделенное состояние, нажмите кнопку «Стоп», чтобы остановить запись информации о производительности.
  • Затем вы можете увидеть информацию панели мониторинга производительности на этапе кликов:

Сосредоточим наше внимание на периоде наибольшей активности ЦП,

Как видно из графика, временные затраты этой части (712 мс) в основном вызваны сценарием, точнее, вызваны выполнением скрипта по событию клика, и это видно из стека вызовов функции и из временной последовательности время в основном тратится наupdateComponentв функции.

Об этом уже можно догадаться раз или два, если вы не уверены, что делает эта функция, то лучше расширитьUser TimingСтолбец, чтобы увидеть более «популярное» потребление времени

Время было потрачено вAppПри обновлении компонента каждый разAppобновления компонентов, что означает, что каждыйItemКомпоненты также обновляются, а это означает, что каждыйItemперерисовываются (выполняютсяrenderфункция)

Если вы все еще сомневаетесь в приведенном выше утверждении или вам трудно это представить, вы можете прямоAppкомпонентrenderфункция иItemкомпонентrenderфункция плюсconsole.log. Затем каждый раз, когда вы нажимаете, вы будете видетьAppвнутреннийconsoleа такжеItemвнутреннийconsoleОба вызываются 10k раз. Учтите, что в это время страница будет отвечать медленнее, т.к. в консоль выводится 10к разconsole.logтакже стоит

Более важным моментом знания является то, что до тех пор, пока состояние компонента (propsилиstate) изменен, то компонент выполняется по умолчаниюrenderфункция для повторного рендеринга (вы также можете переопределитьshouldComponentUpdateВручную предотвратите это, что является пунктом оптимизации, упомянутым ниже). Также обратите внимание, что реализацияrenderФункции не подразумевают, что фактическое дерево DOM в браузере должно быть изменено. Необходимость изменения реального DOM в браузере определяется окончательным сравнением React с виртуальным деревом.Все мы знаем, что изменение реального DOM в браузере — очень затратная вещь, поэтому React сделал оптимизацию за нас. но выполнитьrenderРасходы по-прежнему должны нести мы сами

Таким образом, в этом примере каждый раз, когда щелкают элемент списка, это вызываетitemsсостояние изменяется, а возвращенноеitemsСостояние всегда представляет собой новый массив, поэтому каждый щелчок передается вAppСвойства компонента все новые

дать отпор

Запомните следующую формулу

UI = f(state)

На странице вы видите карту штата. И наоборот, пока состояние компонента или свойства, переданные компоненту, не изменились, компонент не будет повторно визуализирован. Мы можем использовать это, чтобы предотвратитьAppрендеринг, если он гарантированно будет перенаправленAppСвойства компонента не изменятся. В конце концов, неразумно изменять данные только одного элемента списка и вызывать повторную визуализацию других данных 9999.

Но что нужно сделать, чтобы измененные данные передавались вAppданные не меняются?

Изменив структуру данных

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

  1. структура массиваids: Сохраняйте идентификатор только для порядка записи данных, например:[id1, id2, id3]
  2. Структура словаря (объекта)itemskey-valueВ форму записывается конкретная информация о каждом элементе данных:{id1: {marked: false}, id2: {marked: false}}

Код ключа следующий:

function ids(state = [], action) {
  return state;
}

function items(state = {}, action) {
  switch (action.type) {
    case "MARK":
      const item = state[action.id];
      return {
        ...state,
        [action.id]: { ...item, marked: !item.marked }
      };
    default:
      return state;
  }
}

function itemsReducer(state = {}, action) {
  return {
    ids: ids(state.ids, action),
    items: items(state.items, action)
  };
}

const store = createStore(itemsReducer);

class App extends Component {
  render() {
    const { ids } = this.props;
    return (
      <div>
        {ids.map(id => {
          return <Item key={id} id={id} />;
        })}
      </div>
    );
  }
}

// App.js:
function mapStateToProps(state) {
  return { ids: state.ids };
}
// Item.js
function mapStateToProps(state, props) {
  const { id } = props;
  const { items } = state;
  return {
    item: items[id]
  };
}

const markItem = id => ({ type: "MARK", id });
export default connect(mapStateToProps, { markItem })(Item);

При таком способе мышленияItemКомпонент напрямую подключен к Магазину и находится непосредственно по идентификатору каждый раз, когда на него нажимают.itemsИнформация в словаре состояний изменяется. потому чтоAppтолько заботаidsсостояние, а добавления, удаления и изменения не участвуют в этом требовании, поэтомуidsсостояние никогда не меняется, вMountedПозже,AppБольше никогда не будет обновляться. Итак, теперь независимо от того, как вы нажмете на элемент списка, будет обновлен только тот элемент списка, на который нажали.

Я написал статью много лет назад:«Создание модуля управления кэшем в Node.js», в котором упоминалась та же идея решения, есть более подробное описание

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

предотвратить рендеринг

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

Вы уже должны знать, что такая функция существует в жизненном цикле компонента React.shoudlComponentUpdateМожет решить, продолжать ли рендеринг, по умолчанию он возвращаетtrue, то есть всегда перерисовывать, вы также можете переопределить его, чтобы вернутьfalse, что предотвращает рендеринг.

Используя эту функцию жизненного цикла, мы разрешаем толькоmarkedПерерисовывает компоненты, свойства которых изменились до и после:

class Item extends Component {
  constructor() {
    //...
  }
  shouldComponentUpdate(nextProps) {
    if (this.props["marked"] === nextProps["marked"]) {
      return false;
    }
    return true;
  }

Хотя каждый щелчокAppКомпонент по-прежнему перерисовывается, но успешно блокирует другие 9999ItemОтрисовка компонентов

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

class Item extends React.PureComponent

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

Анти-паттерн

Суровая реальность такова, что даже если вы понимаете вышеизложенные знания, вы все равно можете не знать о подводных камнях производительности в повседневном коде.

Например, при установке значения по умолчанию:

<RadioGroup options={this.props.options || []} />

если каждый разthis.props.optionsзначенияnull, это означает, что каждый раз переходил к<RadioGroup />литеральные массивы[], но литеральные массивы иnew Array()Эффект тот же, всегда генерируется новый экземпляр, поэтому на поверхности, хотя компоненту каждый раз передается один и тот же пустой массив, на самом деле это каждый раз новое свойство для компонента, что и будет вызывать рендеринг. Таким образом, правильным способом должно быть сохранение некоторых часто используемых значений в виде переменных:

const DEFAULT_OPTIONS = []
<RadioGroup options={this.props.options || DEFAULT_OPTIONS} />

Другой пример — привязка функции к событию.

<Button onClick={this.update.bind(this)} />

или

<Button
  onClick={() => {
    console.log("Click");
  }}
/>

В обоих случаях каждый раз, когда к компоненту привязывается новая функция, это также вызывает повторный рендеринг. о том какeslintприсоединиться к паре.bindДля обнаружения методов и стрелочных функций, а также для решения, пожалуйста, обратитесь кNo .bind() or Arrow Functions in JSX Props (react/jsx-no-bind)

конец

В следующей статье мы узнаем, как использовать сторонние библиотеки, такие какimmutablejsа такжеreselectоптимизировать проект

Эта статья также была опубликована в моемЗнайте переднюю колонку, приветствую всех, чтобы обратить внимание

использованная литература