Подсказки повествования и образцы кода в этой статье взяты из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);
Этот важный фрагмент кода воплощает в себе несколько ключевых фактов:
- Каждый элемент списка (
item
) структура данных{ id, marked }
- список (
items
) структура данных представляет собой тип массива:[{id1, marked}, {id2, marked}, {id3, marked}]
-
App
Рендеринг списка выполняется путем обхода (map
) массив спискаitems
осуществленный - Когда пользователь нажимает на элемент, щелкнутый элемент
id
Перейти кitem
Редуктор, редуктор пересекаетсяitems
, сравните по одномуid
способ найти элементы, которые необходимо пометить - Вернуть новый массив после перемаркировки
- Новый массив возвращается в
App
,App
визуализировать снова
Если вы не можете собрать воедино приведенный выше фрагмент кода и факты, которые я описал, вы можете найти его на github.полный кодПросмотрите или запустите.
Я считаю, что для такого требования код большинства людей написан таким образом.
Но это имеет низкую производительность, когда приведенный выше код не говорит вам правду. Когда вы пытаетесь нажать на опцию, выделение опции будет задержано как минимум на полсекунды, и пользователь почувствует, что ответ списка замедлен.
Такие значения задержки не являются абсолютными:
- Это явление происходит только тогда, когда количество элементов списка велико, скажем, 10 тыс.
- в среде разработки (
ENV === 'development'
) будет быстрее, чем запуск в продакшене (ENV === 'production'
) работает медленнее - Конфигурация ЦП моего персонального ПК составляет 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
Информация хранится в структуре массива.Важной особенностью структуры массива является обеспечение последовательной непротиворечивости доступа к данным. Теперь мы разделяем данные на две части
- структура массива
ids
: Сохраняйте идентификатор только для порядка записи данных, например:[id1, id2, id3]
- Структура словаря (объекта)
items
:кkey-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
оптимизировать проект
Эта статья также была опубликована в моемЗнайте переднюю колонку, приветствую всех, чтобы обратить внимание