Оптимизация производительности приложения React

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

Когда мы создаем рендеринг компонента, React создает виртуальный DOM для дерева элементов в компоненте. Теперь, когда состояние компонента меняется, React воссоздает виртуальное дерево DOM и сравнивает результат с предыдущим рендером.

Затем он обновляет только измененный элемент в реальном DOM. Этот процесс называется диффирингом.

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

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

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

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

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

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

1. При необходимости сохраняем состояние компонента локальным

import { useState } from "react";

export default function App() {
  const [input, setInput] = useState("");

  return (
    <div>
      <input
        type="text"
        value={input}
        onChange={(e) => setInput(e.target.value)}
      />
      <h3>Input text: {input}</h3>
      <ChildComponent />
    </div>
  );
}

function ChildComponent() {
  console.log("child component is rendering");
  return <div>This is child component.</div>;
};

Вход в полноэкранный режим Выход из полноэкранного режима

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

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

import { useState } from "react";

export default function App() {
  return (
    <div>
      <FormInput />
      <ChildComponent />
    </div>
  );
}

Вход в полноэкранный режим Выйти из полноэкранного режима

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

Это означает, что когда состояние изменяется, только компонент FormInput пересматривается, а ChildComponent больше не пересматривается при каждом нажатии клавиши.

2. React. Lazy для компонентов с ленивой загрузкой

Чтобы реализовать разделение кода, мы преобразуем обычный импорт React следующим образом:

import Home from "./components/Home";
import About from "./components/About";

Вход в полноэкранный режим Выход из полноэкранного режима

А затем в нечто подобное:

const Home = React.lazy(() => import("./components/Home"));
const About = React.lazy(() => import("./components/About"));

Вход в полноэкранный режим Выйти из полноэкранного режима

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

После импорта мы должны отрисовать ленивые компоненты внутри компонента Suspense следующим образом:

<Suspense fallback={<p>Loading page...</p>}>
  <Route path="/" exact>
    <Home />
  </Route>
  <Route path="/about">
    <About />
  </Route>
</Suspense>

Войти в полноэкранный режим Выйти из полноэкранного режима

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

3. React.memo

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

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

import { useState } from "react";

export default function App() {
  const [input, setInput] = useState("");
  const [count, setCount] = useState(0);

  return (
    <div>
      <input
        type="text"
        value={input}
        onChange={(e) => setInput(e.target.value)}
      />
      <button onClick={() => setCount(count + 1)}>Increment counter</button>
      <h3>Input text: {input}</h3>
      <h3>Count: {count}</h3>
      <hr />
      <ChildComponent count={count} />
    </div>
  );
}

function ChildComponent({ count }) {
  console.log("child component is rendering");
  return (
    <div>
      <h2>This is a child component.</h2>
      <h4>Count: {count}</h4>
    </div>
  );
}

Вход в полноэкранный режим Выход из полноэкранного режима

При обновлении поля ввода происходит повторное отображение как компонента App, так и ChildComponent.

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

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

import React, { useState } from "react";

const ChildComponent = React.memo(function ChildComponent({ count }) {
  console.log("child component is rendering");
  return (
    <div>
      <h2>This is a child component.</h2>
      <h4>Count: {count}</h4>
    </div>
  );
});

Вход в полноэкранный режим Выход из полноэкранного режима

Если реквизит count никогда не меняется, React пропустит рендеринг ChildComponent и повторно использует предыдущий результат рендеринга. Это повышает производительность React.

React.memo() работает довольно хорошо, когда мы передаем примитивные значения, такие как число в нашем примере. И, если вы знакомы с ссылочным равенством, примитивные значения всегда ссылочно равны и возвращают true, если значения никогда не меняются.

С другой стороны, непримитивные значения типа object, которые включают массивы и функции, всегда возвращают false между повторными рендерингами, потому что они указывают на разные пространства в памяти.

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

import React, { useState } from "react";

export default function App() {
  // ...

  const incrementCount = () => setCount(count + 1);

  return (
    <div>
      {/* ... */}
      <ChildComponent count={count} onClick={incrementCount} />
    </div>
  );
}

const ChildComponent = React.memo(function ChildComponent({ count, onClick }) {
  console.log("child component is rendering");
  return (
    <div>
      {/* ... */}
      <button onClick={onClick}>Increment</button>
      {/* ... */}
    </div>
  );
});

Войти в полноэкранный режим Выйти из полноэкранного режима

Этот код фокусируется на функции incrementCount, передаваемой ChildComponent. При повторном рендеринге компонента App, даже если кнопка подсчета не нажата, функция переопределяется, заставляя ChildComponent также повторно рендериться.

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

Использование хука useCallback

С помощью хука useCallback функция incrementCount переопределяется только при изменении массива зависимостей count:

const incrementCount = React.useCallback(() => setCount(count + 1), [count]);

Вход в полноэкранный режим Выход из полноэкранного режима

Использование хука useMemo

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

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

Подобно useCallback, хук useMemo также ожидает функцию и массив зависимостей:

const memoizedValue = React.useMemo(() => {
  // return expensive computation
}, []);

Войти в полноэкранный режим Выход из полноэкранного режима

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

import React, { useState } from "react";

const expensiveFunction = (count) => {
  // artificial delay (expensive computation)
  for (let i = 0; i < 1000000000; i++) {}
  return count * 3;
};

export default function App() {
  // ...
  const myCount = expensiveFunction(count);
  return (
    <div>
      {/* ... */}
      <h3>Count x 3: {myCount}</h3>
      <hr />
      <ChildComponent count={count} onClick={incrementCount} />
    </div>
  );
}

const ChildComponent = React.memo(function ChildComponent({ count, onClick }) {
  // ...
});

Вход в полноэкранный режим Выход из полноэкранного режима

Каждый раз при рендеринге компонент App вызывает expensiveFunction и замедляет работу приложения.

Функция expensiveFunction должна вызываться только при нажатии на кнопку подсчета, а не когда мы вводим текст в поле ввода. Мы можем memoize возвращаемое значение expensiveFunction с помощью хука useMemo, чтобы повторно вычислять функцию только тогда, когда это необходимо, т.е. когда нажата кнопка подсчета.

Для этого мы получим что-то вроде этого:

const myCount = React.useMemo(() => {
  return expensiveFunction(count);
}, [count]);

Войти в полноэкранный режим Выход из полноэкранного режима

Методы оптимизации имеют свою цену при неправильном использовании, и обертывание всего в memo или useCallback не сделает ваши приложения быстрыми, но их правильное использование и профилирование на этом пути может стать спасением.

4. Виртуализация окон или списков в приложениях React

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

5. Ленивая загрузка изображений в React

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

Заключение:

Чтобы оптимизировать наше React-приложение, мы должны сначала найти проблему производительности в нашем приложении, которую нужно устранить. В этом руководстве мы объяснили, как измерить производительность React-приложения и как оптимизировать производительность для улучшения пользовательского опыта.

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

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