Оптимизация — это вопрос номер один для каждого разработчика при создании любого программного обеспечения, особенно веб-приложений. React — это библиотека JavaScript для создания пользовательских интерфейсов. React поставляется с несколькими способами минимизации количества дорогостоящих операций DOM, необходимых для обновления пользовательского интерфейса. Использование React позволит создать быстрый пользовательский интерфейс для многих приложений, не прилагая особых усилий для оптимизации производительности.
Когда мы создаем рендеринг компонента, React создает виртуальный DOM для дерева элементов в компоненте. Теперь, когда состояние компонента меняется, React воссоздает виртуальное дерево DOM и сравнивает результат с предыдущим рендером.
Затем он обновляет только измененный элемент в реальном DOM. Этот процесс называется диффирингом.
React использует концепцию виртуального DOM, чтобы минимизировать затраты на производительность при повторном рендеринге веб-страницы, поскольку реальный DOM требует больших затрат на манипуляции.
Проблема возникает, когда дочерние компоненты не затронуты изменением состояния. Другими словами, они не получают никаких свойств от родительского компонента.
Тем не менее, React повторно рендерит эти дочерние компоненты. Таким образом, пока родительский компонент рендерится, все его дочерние компоненты рендерятся независимо от того, передается им реквизит или нет; это поведение React по умолчанию.
- Профилирование приложения React, чтобы понять, где находятся узкие места
- 1. При необходимости сохраняем состояние компонента локальным
- 2. React. Lazy для компонентов с ленивой загрузкой
- 3. React.memo
- Использование хука useCallback
- Использование хука useMemo
- 4. Виртуализация окон или списков в приложениях React
- 5. Ленивая загрузка изображений в 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-приложения и как оптимизировать производительность для улучшения пользовательского опыта.
Если вы считаете эти методы полезными, поделитесь ими с другими, а также я буду рад узнать о других методах, поэтому оставляйте свои комментарии ниже.