Как реализовать мемоизацию в React для повышения производительности?

Как реализовать мемоизацию в React для повышения производительности Программирование и разработка

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

Мы рассмотрим следующее:

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

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

Как React отображает пользовательский интерфейс

Прежде чем вдаваться в подробности мемоизации в React, давайте сначала посмотрим, как React отображает пользовательский интерфейс с помощью виртуальной модели DOM.

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

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

Это виртуальное представление реального DOM. Теперь, когда происходит какое-либо изменение в состоянии приложения, вместо прямого обновления реальной DOM React создает новую виртуальную DOM. Затем React сравнивает этот новый виртуальный DOM с ранее созданным виртуальным DOM, чтобы найти различия, которые необходимо перекрасить.

Читайте также:  Таблица частот с интервалами в R

Используя эти различия, виртуальный DOM будет эффективно обновлять реальный DOM с изменениями. Это повышает производительность, потому что вместо простого обновления элемента пользовательского интерфейса и всех его дочерних элементов виртуальная DOM будет эффективно обновлять только необходимые и минимальные изменения в реальной DOM.

Зачем нужна мемоизация в React

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

Мы создадим родительский класс, содержащий кнопку для увеличения переменной состояния с именем count. Родительский компонент также имеет вызов дочернего компонента, передавая ему опору. Мы также добавили console.log()операторы в метод рендеринга обоих классов:

//Parent.js
class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  handleClick = () => {
    this.setState((prevState) => {
      return { count: prevState.count + 1 };
    });
  };

  render() {
    console.log("Parent render");
    return (
      <div className="App">
        <button onClick={this.handleClick}>Increment</button>
        <h2>{this.state.count}</h2>
        <Child name={"joe"} />
      </div>
    );
  }
}

export default Parent;

Полный код этого примера доступен на CodeSandbox.

Мы создадим Childкласс, который принимает опору, переданную родительским компонентом, и отображает ее в пользовательском интерфейсе:

//Child.js
class Child extends React.Component {
  render() {
    console.log("Child render");
    return (
      <div>
        <h2>{this.props.name}</h2>
      </div>
    );
  }
}

export default Child;

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

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

Parent render
Child render
Parent render
Child render
Parent render
Child render

Вы можете увеличить счетчик для вышеприведенного примера самостоятельно в следующей песочнице и увидеть консоль для вывода:

import { StrictMode } from «react»;
import ReactDOM from «react-dom»;

 

import Parent from «./Parent»;

 

const rootElement = document.getElementById(«root»);
ReactDOM.render(
<StrictMode>
<Parent />
</StrictMode>,
rootElement
);

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

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

Мемоизация в React

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

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

Реализация мемоизации в компоненте класса

Чтобы реализовать мемоизацию в компоненте класса, мы будем использовать React.PureComponent. React.PureComponentреализует shouldComponentUpdate (), который выполняет неглубокое сравнение состояния и свойств и отображает компонент React только в случае изменения свойств или состояния.

Измените дочерний компонент на код, показанный ниже:

//Child.js
class Child extends React.PureComponent { // Here we change React.Component to React.PureComponent
  render() {
    console.log("Child render");
    return (
      <div>
        <h2>{this.props.name}</h2>
      </div>
    );
  }
}

export default Child;

Полный код этого примера показан в следующей песочнице:

import { StrictMode } from «react»;
import ReactDOM from «react-dom»;

 

import Parent from «./Parent»;

 

const rootElement = document.getElementById(«root»);
ReactDOM.render(
<StrictMode>
<Parent />
</StrictMode>,
rootElement
);

Родительский компонент остается без изменений. Теперь, когда мы увеличиваем счетчик в родительском компоненте, вывод в консоли будет следующим:

Parent render
Child render
Parent render
Parent render

Для первого рендеринга он вызывает методы рендеринга как родительского, так и дочернего компонента.

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

Реализация мемоизации в функциональном компоненте

Чтобы реализовать мемоизацию в функциональных компонентах React, мы будем использовать React.memo (). React.memo()- это компонент более высокого порядка (HOC), который выполняет аналогичную работу PureComponent, избегая ненужных повторных отрисовок.

Ниже приведен код функционального компонента:

//Child.js
export function Child(props) {
  console.log("Child render");
  return (
    <div>
      <h2>{props.name}</h2>
    </div>
  );
}

export default React.memo(Child); // Here we add HOC to the child component for memoization

Мы также преобразуем родительский компонент в функциональный компонент, как показано ниже:

//Parent.js
export default function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
  };
  console.log("Parent render");
  return (
    <div>
      <button onClick={handleClick}>Increment</button>
      <h2>{count}</h2>
      <Child name={"joe"} />
    </div>
  );
}

Полный код этого примера можно увидеть в следующей песочнице:

import { StrictMode } from «react»;
import ReactDOM from «react-dom»;
import Parent from «./Parent»;

 

const rootElement = document.getElementById(«root»);
ReactDOM.render(
<StrictMode>
<Parent />
</StrictMode>,
rootElement
);

Теперь, когда мы увеличиваем счетчик в родительском компоненте, на консоль выводится следующее:

Parent render
Child render
Parent render
Parent render
Parent render

Проблема с React.memo () для свойств функций

В приведенном выше примере мы видели, что когда мы использовали React.memo()HOC для дочернего компонента, дочерний компонент не перерисовывался, даже если это делал родительский компонент.

Однако следует иметь в виду небольшую оговорку: если мы передадим функцию в качестве опоры дочернему компоненту, даже после использования React.memo()дочерний компонент будет повторно отрисован. Давайте посмотрим на это на примере.

Мы изменим родительский компонент, как показано ниже. Здесь мы добавили функцию-обработчик, которую передадим дочернему компоненту в качестве свойств:

//Parent.js
export default function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
  };

  const handler = () => {
    console.log("handler");    // This is the new handler that will be passed to the child
  };

  console.log("Parent render");
  return (
    <div className="App">
      <button onClick={handleClick}>Increment</button>
      <h2>{count}</h2>
      <Child name={"joe"} childFunc={handler} />
    </div>
  );
}

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

//Child.js
export function Child(props) {
  console.log("Child render");
  return (
    <div>
      <h2>{props.name}</h2>
    </div>
  );
}

export default React.memo(Child);

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

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

В следующем разделе мы увидим, как решить эту проблему.

useCallback() чтобы избежать повторного рендеринга

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

Чтобы избежать повторного создания функции каждый раз при рендеринге родительского компонента. Мы будем использовать перехватчик React, называемый useCallback (). Хуки были введены в React 16. Чтобы узнать больше о хуках, вы можете взглянуть на официальную документацию по хукам React или почитать » React Hooks: Как начать работу и создать свой собственный «.

useCallback()Крюк принимает два аргумента: функцию обратного вызова, и список зависимостей.

Рассмотрим следующий пример useCallback():

const handleClick = useCallback(() => {
  //Do something
}, [x,y]);

Здесь useCallback()добавляется к handleClick()функции. Второй аргумент [x,y]может быть пустым массивом, отдельной зависимостью или списком зависимостей. Всякий раз, когда любая зависимость, упомянутая во втором аргументе, изменяется, только тогда handleClick()функция будет воссоздана.

Если зависимости, упомянутые в useCallback(), не изменяются, возвращается мемоизированная версия обратного вызова, указанная в качестве первого аргумента. Мы изменим наш родительский функциональный компонент, чтобы использовать useCallback()перехватчик для обработчика, который передается дочернему компоненту:

//Parent.js
export default function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
  };

  const handler = useCallback(() => { //using useCallback() for the handler function
    console.log("handler");
  }, []);

  console.log("Parent render");
  return (
    <div className="App">
      <button onClick={handleClick}>Increment</button>
      <h2>{count}</h2>
      <Child name={"joe"} childFunc={handler} />
    </div>
  );
}

Код дочернего компонента остается без изменений.

Полный код этого примера показан ниже:

import { StrictMode } from «react»;
import ReactDOM from «react-dom»;
import Parent from «./Parent»;

 

const rootElement = document.getElementById(«root»);
ReactDOM.render(
<StrictMode>
<Parent />
</StrictMode>,
rootElement
);

Когда мы увеличиваем счетчик в родительском компоненте для приведенного выше кода, мы можем увидеть следующий результат:

Parent render
Child render
Parent render
Parent render
Parent render

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

То, что нужно запомнить

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

  • возвращает тот же результат при одинаковых реквизитах
  • имеет несколько элементов пользовательского интерфейса, и проверка виртуальной DOM повлияет на производительность
  • часто предоставляется один и тот же реквизит

Заключение

В этом уроке мы увидели:

  • как React отображает пользовательский интерфейс
  • зачем нужна мемоизация
  • как реализовать мемоизацию в React через React.memo()функциональный компонент React и React.PureComponent компонент класса
  • вариант использования, когда даже после использования React.memo()дочерний компонент будет повторно отрисовывать
  • как использовать useCallback()ловушку, чтобы избежать повторного рендеринга, когда функция передается в качестве свойств дочернему компоненту.

Надеюсь, вы нашли это введение в мемоизацию React полезным!

Оцените статью
bestprogrammer.ru
Добавить комментарий