Главная » Статьи » 5 методов оптимизации производительности React

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

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

От автора: Оптимизация производительности приложений является ключевым моментом для разработчиков, которые заботятся о том, чтобы оценка пользователей оставалась положительной и чтобы они продолжали пользоваться приложением.

Согласно исследованию Akamai, секундная задержка во времени загрузки может привести к снижению конверсии на 7%, что заставляет разработчиков создавать приложения с оптимизированной производительностью.

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

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

Сохранение состояния компонента локальным, где это необходимо

Компоненты Memoizing React для предотвращения ненужных повторных рендеров

Разделение кода в React с помощью динамического импорта

Виртуализация окон или списков в React

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

Техники предварительной оптимизации React

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

Понимание того, как React обновляет пользовательский интерфейс

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

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

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

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

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

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

Давайте продемонстрируем эту концепцию. Например, у нас есть компонент App, содержащий некоторое состояние и дочерний компонент:

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 обновляется, ChildComponent выполняет повторную визуализацию, даже если на него не влияет изменение состояния напрямую.

Откройте консоль в демонстрации на CodeSandbox и напишите что-нибудь в поле ввода. Мы увидим, что при каждом нажатии клавиши ChildComponent обновляется.

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

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

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

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

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

Чтобы использовать Profiler, мы должны установить React DevTools для браузера. Если он у вас еще не установлен, перейдите на страницу расширения и установите его (выберите для Chrome или для Firefox). Вы должны увидеть вкладку Profiler при работе над проектом React.

Вернемся к нашему коду, если мы профилируем приложение, мы увидим следующее поведение:

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

DevTools Profiler выделяет каждый визуализированный компонент, в то время как поле ввода текста обновляется, и мы получаем каждую деталь визуализации компонентов.

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

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

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

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

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

Методы оптимизации производительности React

1. Сохранение состояния компонента там, где это необходимо

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

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

import { useState } from "react"; export default function App() { return ( <div> <FormInput /> <ChildComponent /> </div> );
} function FormInput() { const [input, setInput] = useState(""); return ( <div> <input type="text" value={input} onChange={(e) => setInput(e.target.value)} /> <h3>Input text: {input}</h3> </div> );
} function ChildComponent() { console.log("child component is rendering"); return <div>This is child component.</div>;
}

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

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

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

2. Компоненты Memoizing React для предотвращения ненужных повторных рендеров

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

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

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

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 должен повторно визуализироваться только при нажатии кнопки подсчета. В этом случае мы можем мемоизировать ChildComponent.

Использование React.memo()

Обернув чисто функциональный компонент в 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 и повторно использует предыдущий результат рендеринга. Следовательно, улучшается производительность приложения. Вы можете попробовать это в примере на CodeSandbox.

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

Непримитивные значения, такие как 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]);

Вы можете попробовать это сами на CodeSandbox.

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

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

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

3. Разделение кода в React с помощью динамического импорта

Разделение кода — еще один важный метод оптимизации для приложения React. По умолчанию, когда приложение React отображается в браузере, «пакетный» файл, содержащий весь код приложения, сразу загружается и используется пользователями. Этот файл создается путем объединения всех файлов кода, необходимых для работы веб-приложения.

Идея объединения полезна, поскольку уменьшает количество HTTP-запросов, которые обрабатывает страница. Однако по мере роста приложения размеры файлов увеличиваются, что увеличивает размер «пакетныйого» файла. В определенный момент это непрерывное увеличение файла замедляет начальную загрузку страницы, снижая удовлетворенность у пользователей.

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

Чтобы реализовать разделение кода, мы преобразуем обычный импорт 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 загружает файл только для запрошенной страницы, а не загружает большой файл пакета для всего приложения.

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

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

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

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

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

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

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

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

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

react-lazyload и react-lazy-load-image-component — популярные библиотеки отложенной загрузки, которые можно использовать в проектах React.

Заключение

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

Автор: Ibadehin Mojeed

Источник: blog.logrocket.com

Редакция: Команда webformyself.

Читайте нас в Telegram, VK, Яндекс.Дзен