Статья впервые опубликованаличный блог
предисловие
Цель
В этой статье представлены только методы оптимизации производительности, характерные для функциональных компонентов, и не представлены как компоненты класса, так и функциональные компоненты, такие как использование ключей. Кроме того, в этой статье подробно не рассказывается об использовании API, и она может быть написана позже, ведь правильно использовать хуки довольно сложно.
Читатель
Я практиковал функциональные компоненты React, и я практиковал хуки.Я как минимум прочитал документацию для useState, useCallback и useMemo API.Если у вас есть опыт оптимизации производительности компонентов класса, то эта статья сделает вас немного знакомым. ощущение.
Идеи оптимизации эффективности реагирования на реагирование
Я думаю, что основными направлениями философии оптимизации производительности React являются следующие два:
-
Уменьшите количество повторных рендеров. Поскольку самая тяжелая (и самая длинная) часть в React — это согласование (просто понимаемое как diff), если вы не отрендерите, согласования не будет.
-
Уменьшите количество вычислений. Основная цель — уменьшить количество повторных вычислений.Для функциональных компонентов каждый рендер будет повторно выполнять вызов функции с нуля.
При использовании компонентов класса в основном используются следующие API-интерфейсы оптимизации React:shouldComponentUpdate
а такжеPureComponent
, решения, предоставляемые этими двумя API, предназначены дляУменьшите количество повторных рендеров, главным образом, чтобы уменьшить ситуацию, когда родительский компонент обновляется, а дочерний компонент также обновляется.Хотя также возможно предотвратить рендеринг текущего компонента при обновлении состояния, если вы хотите сделать это, это доказывает, что ваше свойство не подходит в качестве состояния, но его следует использовать как статическое свойство или поместить вне класса как простую переменную.
Но в функциональных компонентах нет цикла объявления и нет класса, так как же оптимизировать производительность?
React.memo
Первое, что нужно представить, этоReact.memo
можно сказать, что этот API находится в компоненте бенчмаркингаPureComponent
, что может уменьшить количество повторных рендеров.
Примеры возможных проблем с производительностью
Например 🌰, сначала рассмотрим два фрагмента кода:
В корневом каталоге есть index.js, код такой, реализация наверное такая: вверху заголовок, посередине кнопка (нажмите на кнопку, чтобы изменить заголовок), внизу марионеточный компонент, и передайте имя внутрь.
// index.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
import Child from './child'
function App() {
const [title, setTitle] = useState("这是一个 title")
return (
<div className="App">
<h1>{ title }</h1>
<button onClick={() => setTitle("title 已经改变")}>改名字</button>
<Child name="桃桃"></Child>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
В том же каталоге есть child.js
// child.js
import React from "react";
function Child(props) {
console.log(props.name)
return <h1>{props.name}</h1>
}
export default Child
Эффект при первом рендеринге выглядит следующим образом:
и консоль напечатает"桃桃”
, что доказывает, что дочерний компонент визуализируется.
Следующий кликизменить имяЭта кнопка, страница станет:
заголовок изменился, и консоль также печатает"桃桃"
, вы можете видеть, что хотя мы изменили состояние родительского компонента, родительский компонент повторно визуализируется, и дочерний компонент также повторно визуализируется. Вы можете подумать, что реквизиты, переданные дочернему компоненту, не изменились, было бы неплохо, если бы дочерний компонент не перерисовывался, почему вы так думаете?
Мы предполагаем, что дочерний компонент является очень большим компонентом, и его рендеринг один раз потребует много производительности, тогда мы должны попытаться уменьшить рендеринг этого компонента, иначе это легко вызовет проблемы с производительностью, поэтому, если дочерний компонент не измените пропсы, даже если родительский компонент перерисовывается, а дочерний компонент тоже не должен визуализироваться.
Итак, как мы можем сделать так, чтобы дочерний компонент не отображался, если свойства не изменились?
Ответ заключается в использованииReact.memo
Отрисовывает тот же результат с теми же реквизитами и повышает производительность компонента, запоминая, как компонент визуализирует результат.
Базовое использование React.memo
Передайте объявленный компонент черезReact.memo
Просто заверните его.React.memo
На самом деле это функция более высокого порядка, которая передает компонент и возвращает запомненный компонент.
function Component(props) {
/* 使用 props 渲染 */
}
const MyComponent = React.memo(Component);
Затем компонент Child из приведенного выше примера можно изменить на это:
import React from "react";
function Child(props) {
console.log(props.name)
return <h1>{props.name}</h1>
}
export default React.memo(Child)
пройти черезReact.memo
Когда реквизиты обернутого компонента остаются неизменными, обернутый компонент не будет перерисовываться, то есть в приведенном выше примере, после того, как я нажму, чтобы изменить имя, изменится только заголовок, но дочерний компонент не будет быть перерендерен (эффект заключается в том, что журнал Child не будет напечатан на консоли), а результат последнего рендеринга будет непосредственно повторно использован.
Этот эффект в основном такой же, как и в компоненте класса.PureComponent
Эффект очень похож, за исключением того, что первый используется для функциональных компонентов, а второй — для компонентов класса.
Расширенное использование React.memo
По умолчанию он будет выполнять только неглубокое сравнение сложных объектов реквизита (неглубокое сравнение будет сравнивать только то, одинаковы ли ссылки двух объектов реквизита до и после, и не будет сравнивать, является ли содержимое объектов одинаковым). ) Если вы хотите контролировать процесс сравнения, пожалуйста, передайте пользовательскую функцию сравнения через второй параметр для реализации.
function MyComponent(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/*
如果把 nextProps 传入 render 方法的返回结果与
将 prevProps 传入 render 方法的返回结果一致则返回 true,
否则返回 false
*/
}
export default React.memo(MyComponent, areEqual);
Эта часть исходит изРеагировать на официальный сайт.
Если вы использовали его в компоненте классаshouldComponentUpdate()
Таким образом, вы будетеReact.memo
Второй параметр очень знаком, но стоит отметить, что если реквизиты равны,areEqual
вернусьtrue
; если свойства не равны, вернутьfalse
. Это то же самое, чтоshouldComponentUpdate
Возвращаемое значение метода противоположно.
useCallback
Теперь, согласно приведенному выше примеру, снова измените требования, добавьте подзаголовок к вышеуказанным требованиям и создайте кнопку для изменения подзаголовка, а затем поместите кнопку для изменения заголовка в дочерний компонент.
Целью размещения кнопки, которая изменяет заголовок, в дочернем компоненте, является передача события изменения заголовка в дочерний компонент через реквизиты, а затем наблюдение за этим событием может вызвать проблемы с производительностью.
Сначала посмотрите на код:
Родительский компонент index.js
// index.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
import Child from "./child";
function App() {
const [title, setTitle] = useState("这是一个 title");
const [subtitle, setSubtitle] = useState("我是一个副标题");
const callback = () => {
setTitle("标题改变了");
};
return (
<div className="App">
<h1>{title}</h1>
<h2>{subtitle}</h2>
<button onClick={() => setSubtitle("副标题改变了")}>改副标题</button>
<Child onClick={callback} name="桃桃" />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
дочерний компонент
import React from "react";
function Child(props) {
console.log(props);
return (
<>
<button onClick={props.onClick}>改标题</button>
<h1>{props.name}</h1>
</>
);
}
export default React.memo(Child);
Эффект первого рендера
Этот код будет выглядеть как изображение выше при первом отображении, и консоль выведет桃桃
.
затем, когда я нажимаюизменить субтитрыПосле этой кнопки субтитр изменится на «Субтитры изменены», и консоль снова распечатает桃桃
, что доказывает, что дочерний компонент был перерендерен еще раз, но дочерний компонент не изменился, поэтому повторный рендеринг дочернего компонента на этот раз избыточен, так как же избежать этого избыточного рендеринга?
найти причину
Прежде чем мы решим проблему,Прежде всего, в чем причина этой проблемы?
Разберем, перерисовывается компонент, вообще бывает три случая:
-
Либо изменяется собственное состояние компонента
-
Либо родительский компонент повторно визуализируется, что приводит к повторному рендерингу дочернего компонента, но реквизиты родительского компонента не были изменены.
-
Либо родительский компонент повторно рендерится, вызывая повторный рендеринг дочернего компонента, но реквизиты, переданные родительским компонентом, изменяются.
Затем используйте метод исключения, чтобы узнать, что вызвало это:
Первый явно исключается, когда щелчокизменить субтитрыКогда это не меняет состояние дочернего компонента;
Во втором случае задумайтесь, это просто знакомство?React.memo
При повторном рендеринге родительского компонента реквизиты, переданные от родительского компонента к дочернему, не изменились, но дочерний компонент был повторно отрендерен.В настоящее время мы используемReact.memo
решить эту проблему, поэтому эта ситуация также исключена.
Затем есть третий случай. Когда родительский компонент перерисовывается, свойства, переданные дочернему компоненту, изменились. Посмотрите на два свойства, переданные дочернему компоненту, одно из нихname
,одинonClick
,name
передана константа, она не изменится, какие измененияonClick
Теперь, почему функция обратного вызова, переданная в onClick, изменяется? Как я сказал в начале статьи, каждый раз, когда функциональный компонент перерисовывается, функциональный компонент будет запускаться заново и выполняться повторно, поэтому функция обратного вызова, созданная дважды, должна была измениться, что вызвало повторный рендеринг подкомпонента. оказывать.
Как решить
Найдите причину проблемы, затем решение состоит в том, чтобы сохранить согласованность ссылки на две функции при повторном рендеринге, когда функция не изменилась.useCallback
Это API.
Как использовать обратного вызова
const callback = () => {
doSomething(a, b);
}
const memoizedCallback = useCallback(callback, [a, b])
Передайте функцию и зависимости в качестве аргументовuseCallback
, который вернет мемоизированную версию функции обратного вызова, этот memoizedCallback будет обновляться только при изменении зависимостей.
Затем вы можете изменить index.js на это:
// index.js
import React, { useState, useCallback } from "react";
import ReactDOM from "react-dom";
import Child from "./child";
function App() {
const [title, setTitle] = useState("这是一个 title");
const [subtitle, setSubtitle] = useState("我是一个副标题");
const callback = () => {
setTitle("标题改变了");
};
// 通过 useCallback 进行记忆 callback,并将记忆的 callback 传递给 Child
const memoizedCallback = useCallback(callback, [])
return (
<div className="App">
<h1>{title}</h1>
<h2>{subtitle}</h2>
<button onClick={() => setSubtitle("副标题改变了")}>改副标题</button>
<Child onClick={memoizedCallback} name="桃桃" />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Таким образом, мы можем видеть, что он будет напечатан только на первом рендере.Персик персик, когда вы нажимаете, чтобы изменить подзаголовок и изменить заголовок, он не будет напечатанперсикиз.
Если наш обратный вызов передает параметры и требует повторного добавления кеша при изменении параметров, вы можете поместить параметры в массив второго параметра useCallback как форму зависимости, аналогичную useEffect.
useMemo
Как упоминалось в начале статьи, есть два основных направления оптимизации производительности React: одно — уменьшить количество повторных рендеров (или уменьшить ненужный рендеринг), а другое — уменьшить объем вычислений.
ранее представленныйReact.memoа такжеuseCallbackВсе для уменьшения количества повторных рендеров. Как уменьшить количество вычислений — это то, что делает useMemo, давайте рассмотрим пример.
function App() {
const [num, setNum] = useState(0);
// 一个非常耗时的一个计算函数
// result 最后返回的值是 49995000
function expensiveFn() {
let result = 0;
for (let i = 0; i < 10000; i++) {
result += i;
}
console.log(result) // 49995000
return result;
}
const base = expensiveFn();
return (
<div className="App">
<h1>count:{num}</h1>
<button onClick={() => setNum(num + base)}>+1</button>
</div>
);
}
Эффект первого рендеринга следующий:
Этот пример функции очень прост, просто нажмите+1кнопку, то текущее значение (число) будет добавлено к значению после вызова функции вычисления (expensiveFn), а затем сумма будет установлена на число и отобразится, а консоль выведет49995000
.
Может вызвать проблемы с производительностью
Даже у, казалось бы, простого компонента могут быть проблемы с производительностью.Давайте возьмем этот простейший пример, чтобы увидеть, что еще стоит оптимизировать.
Сначала мы рассматриваем функцию «дорогаяFn» как функцию, требующую значительных вычислительных ресурсов (например, вы можете заменить «i» на «10000000»), а затем, когда мы каждый раз нажимаем+1Когда кнопка нажата, компонент будет перерендерен, и будет вызвана функция «дорогаяFn» и выведено49995000
. Так как значение, возвращаемое при каждом вызове дорогегоFn, является одним и тем же, мы можем найти способ кэшировать вычисленное значение и напрямую возвращать кэшированное значение каждый раз при вызове функции, чтобы мы могли выполнить некоторую оптимизацию производительности.
useMemo для кэширования результатов вычислений
В ответ на вышеперечисленные проблемы можно использовать useMemo для кэширования значения дорогаяFn после выполнения функции.
Во-первых, давайте представим основное использование useMemo, подробное использование можно увидетьОфициальный сайт:
function computeExpensiveValue() {
// 计算量很大的代码
return xxx
}
const memoizedValue = useMemo(computeExpensiveValue, [a, b]);
Первый параметр useMemo — это функция, значение, возвращаемое этой функцией, будет кэшироваться, и это значение будет использоваться в качестве возвращаемого значения useMemo. Второй параметр — это зависимость от массива. Если значение в массиве изменится, оно будет be Повторно выполнить функцию в первом параметре и кэшировать значение, возвращаемое функцией, как возвращаемое значение useMemo.
Поняв, как использовать useMemo, вы можете оптимизировать приведенный выше пример.Оптимизированный код выглядит следующим образом:
function App() {
const [num, setNum] = useState(0);
function expensiveFn() {
let result = 0;
for (let i = 0; i < 10000; i++) {
result += i;
}
console.log(result)
return result;
}
const base = useMemo(expensiveFn, []);
return (
<div className="App">
<h1>count:{num}</h1>
<button onClick={() => setNum(num + base)}>+1</button>
</div>
);
}
Выполните приведенный выше код, и теперь мы можем наблюдать, куда бы мы ни нажимали.+1Сколько раз, выводить только один раз49995000, что означает, что дорогаяFn выполняется только один раз, достигая желаемого эффекта.
резюме
Сценарии использования useMemo в основном используются дляКэшировать результат функции с относительно большим объемом вычислений, что позволяет избежать ненужных повторных вычислений.Студенты, имеющие опыт использования Vue, могут почувствовать, что это имеет тот же эффект, что и вычисляемые свойства в Vue.
Но еще два напоминания
1. Если массив зависимостей не указан,
useMemo
Новое значение рассчитывается при каждом рендеринге;2. Если объем расчета очень мал, вы также можете отказаться от использования useMemo, потому что эта оптимизация не будет основным узким местом производительности, но может вызвать некоторые проблемы с производительностью из-за неправильного использования.
Суммировать
Для небольших проектов узкие места в производительности могут встречаться реже, ведь объем вычислений невелик, а бизнес-логика несложная, а вот для крупных проектов узкие места в производительности, скорее всего, будут встречаться, но есть много аспектов для оптимизации производительности: сеть, ключ С точки зрения рендеринга пути, упаковки, изображений, кэширования и т. д., вы должны проверить, какие аспекты должны быть оптимизированы.Эта статья представляет только верхушку айсберга в области оптимизации производительности: оптимизация React во время работы.
- Направление оптимизации React: уменьшить количество рендеров, уменьшить повторные вычисления.
- См. раздел useCallback о том, как найти методы в React, вызывающие проблемы с производительностью.
- Разумное разделение компонентов на самом деле может оптимизировать производительность.Если вы думаете об этом таким образом, если у вас есть только один большой компонент на всей странице, то при изменении реквизита или состояния необходимо согласовать весь компонент.На самом деле, вы только что изменили текст. Если вы делаете разумное разделение компонентов, вы можете контролировать более мелкие обновления.
Разумное разделение компонентов имеет много других преимуществ, таких как простота обслуживания, и это первый шаг в изучении идеи компонентизации. Разумное разделение компонентов - это еще одно искусство. Если разделение необоснованно, это может привести к путанице состояния, набирать больше кода и больше думать.
рекомендуемая статья
Здесь я расскажу только о методе оптимизации функциональных компонентов.Более подробно о методах оптимизации React вы можете прочитать в следующих статьях:
- 21 совет по оптимизации производительности React
- Говоря о направлении оптимизации производительности React
постскриптум
Я Тао Вэн, фронтендер, который любит думать. Если вы хотите узнать больше о фронтенде, пожалуйста, обратите внимание на мой официальный аккаунт: «Фронтенд Таоюань». Если вы хотите присоединиться к группе по обмену , подпишитесь на официальную учетную запись и ответьте на «WeChat», чтобы привлечь вас. группа