Простые способы повышения производительности вашего приложения на React без использования useMemo
Несмотря на то, что использование useMemo и useCallback совершенно оправдано во многих случаях, и это отличные инструменты, они могут оказаться проблемными при неправильной эксплуатации. Поэтому я собираюсь перечислить несколько аспектов, о которых следует помнить, когда ваше приложение работает неэффективно.
@React – react в телеграме.
1. Перестаньте часто использовать useEffect!
Как разработчики React, мы знаем, что useEffect может оказаться полезным для побочных процессов, таких как обновление переменной состояния.
Эффекты запускаются после рендеринга компонента и фиксации всех узлов в DOM, что означает, что, например, если у вас есть эффект, который проверяет значения prop
и, соответственно, изменяет состояние компонента в соответствии с этими значениями, React сделает следующее:
- Рендеринг компонента и фиксация в DOM.
- Запустит эффект и изменит его состояние.
- Выполненный в эффекте установщик состояния вызовет повторный рендеринг компонента.
- Снова выполнит рендеринг и зафиксирует в DOM.
Вы заметили что-то странное в его поведении?
Компонент осуществляет повторный рендеринг без необходимости!
Чтобы избежать этого, команда react даёт следующий совет:
Чтобы избежать ненужных проходов рендеринга, преобразуйте все данные на верхнем уровне ваших компонентов. Этот код будет автоматически выполняться заново при каждом изменении реквизитов или состояния.
Чтобы пояснить мысль, посмотрите на этот простой компонент:
const AddNumbers = ({ numArray }) => {
const [sum, setSum] = useState(0);
useEffect(() => {
if (numArray.length) {
const currentSum = numArray.reduce(
(accumulator, currentNum) => accumulator + currentNum
);
setSum(currentSum);
}
}, [numArray]);
return (
<>
<h1>Sum: {sum}</h1>
</>
);
};
Компонент принимает массив, добавляет значения и отображает результат. Довольно просто, но запуск эффекта вызывает ненужный повторный рендеринг.
Теперь посмотрите на этот код без эффекта useEffect:
const AddNumbers = ({ numArray }) => {
const sum = (numArray.length ? numArray.reduce(
(accumulator, currentNum) => accumulator + currentNum
) : 0);
return (
<>
<h1>Sum: {sum}</h1>
</>
);
};
Мы не только убрали ненужный повторный рендеринг, но и полностью избавились от необходимости в состоянии, а компонент по-прежнему обновляется при изменении реквизитов!
См. этот фрагмент из новой документации по react:
Если что-то можно вычислить на основе существующих реквизитов, не помещайте это в состояние. Вместо этого вычисляйте это во время рендеринга. Это делает ваш код быстрее (вы избегаете дополнительных “каскадных” обновлений), проще (вы удаляете часть кода) и менее подверженным ошибкам (вы избегаете ошибок, вызванных тем, что различные переменные состояния не синхронизируются друг с другом).
2. Будьте аккуратнее с иерархией компонентов
Иногда для повышения производительности и минимизации повторного рендеринга может быть достаточно изменить структуру компонентов.
Мы все знаем знаменитый шаблон React, позволяющий поднимать состояние вверх, когда нескольким дочерним компонентам одного родителя требуется общее состояние. Мы также знаем знаменитый шаблон проектирования “Container-presentational”, когда мы храним логику в компоненте-контейнере, а дочерние компоненты используем только для рендеринга и пользовательского интерфейса.
Всё это хорошо, но иногда нужно отойти от традиционных шаблонов и посмотреть на вещи с другой точки зрения.
Допустим, у нас есть компонент Form
внутри контейнера с некоторыми другими элементами пользовательского интерфейса, которые могут занять некоторое время для повторного отображения из-за их сложности. Мы располагаем обработчики событий для form
в контейнере и передаём их все через реквизиты формы, не помещая в них логику.
Если мы подумаем об этом, то каждый раз, когда входы формы изменяются, каждый раз, когда пользователь вводит или отправляет форму, родительский компонент будет не только вызывать повторное отображение формы, но и сам будет повторно отображаться! Почему?
Да потому, что установщики и обработчики состояния формы находятся внутри родительского компонента, а все установщики состояния вызывают перерисовку. Теперь представьте, что с каждым нажатием клавиши будет перерисовываться всё, что находится рядом с ней.
Это может оказаться проблематичным в зависимости от размера и сложности остальных компонентов внутри родителя.
У нас есть кусок логики, который нужен только форме, так почему бы просто не перенести его в форму. В этом случае логика будет находиться в нижнем узле и не повлияет ни на одного из его братьев или сестер, ни на его родителя.
Вот пример:
Обратите внимание, что состояние и обработчики находятся в родительском компоненте, что очень часто встречается в приложениях React.
import "./styles.css";
import { useState } from "react";
const Sibling = () => {
return <div className="sibling">Sibling Component</div>;
};
const AddNumbers = ({ handler, submitHandler, num }) => {
return (
<>
<input onChange={handler} type="number" />
<button onClick={submitHandler}>Set number</button>
</>
);
};
export default function App() {
const [num, setNum] = useState(0);
const numberHandler = (e) => {
setNum(e.currentTarget.value);
};
const submitHandler = () => {
alert(`num is: ${num}`);
};
return (
<div className="App">
<AddNumbers
handler={numberHandler}
submitHandler={submitHandler}
num={num}
/>
<div className="container">
<Sibling />
<Sibling />
<Sibling />
<Sibling />
<Sibling />
</div>
</div>
);
}
Если мы откроем профилировщик React-devtools и включим подсветку компонентов для повторного рендеринга, то увидим, что все компоненты внутри родительского компонента рендерятся без необходимости, поскольку мы устанавливаем переменную состояния при каждом нажатии клавиши. Сейчас это небольшой пример, чтобы показать, как это работает, но в большом приложении это может вызвать сотни повторных рендерингов.
Теперь, если мы переместим состояние вниз к единственному компоненту, которому оно необходимо, который в настоящее время является только презентационным компонентом, мы можем остановить это ненужное повторное отображение и инкапсулировать состояние.
Смотрите следующий код после перемещения состояния вниз:
const Sibling = () => {
return <div className="sibling">Sibling Component</div>;
};
const AddNumbers = () => {
const [num, setNum] = useState(0);
const numberHandler = (e) => {
setNum(e.currentTarget.value);
};
const submitHandler = () => {
alert(`num is: ${num}`);
};
return (
<>
<input onChange={numberHandler} type="number" />
<button onClick={submitHandler}>Set number</button>
</>
);
};
export default function App() {
return (
<div className="App">
<AddNumbers />
<div className="container">
<Sibling />
<Sibling />
<Sibling />
<Sibling />
<Sibling />
</div>
</div>
);
}
Как вы можете видеть, состояние было перенесено вниз к компоненту, который в нём нуждается, удалив логику из контейнера. Если мы откроем профилировщик производительности сейчас, мы больше не увидим повторного рендеринга родительского и дочерних компонентов, когда пользователь набирает ввод, или когда изменяется состояние числа, что сэкономит нам тонны рендеров.
3. Используйте React.lazy и Suspense.
Другим советом может быть использование React.lazy
и Suspense
для ленивой загрузки компонентов, которые не сразу необходимы для первоначального представления, например, тех, которые нужны только для определённых взаимодействий с пользователем, или тех, которые потребуются только позже в потоке приложения.
import React, { lazy, Suspense } from "react";
// Lazy load the animation component
const Animation = lazy(() => import("./Animation"));
function App() {
return (
<div className="App">
<h1>Welcome to My App!</h1>
<Suspense fallback={<div>Loading...</div>}>
<Animation />
</Suspense>
</div>
);
}
export default App;
В этом примере мы используем React.lazy
для ленивой загрузки компонента Animation
, когда он необходим, вместо того, чтобы загружать его вместе с остальным приложением при первоначальной загрузке страницы.
Мы также используем Suspense
для обеспечения резервного пользовательского интерфейса во время загрузки компонента. В данном случае мы просто выводим сообщение о загрузке, но вы также можете вывести волчок или другой элемент пользовательского интерфейса, чтобы показать, что что-то происходит в фоновом режиме.
После загрузки компонента Animation
он будет отображаться так же, как и любой другой компонент. Это может помочь улучшить производительность вашего приложения за счёт сокращения времени начальной загрузки, особенно если у вас есть большие или сложные компоненты, которые не нужны сразу.
Заключение
Как вы можете видеть, есть некоторые настройки, которые мы можем сделать для улучшения производительности нашего приложения без использования useMemo
или useCallback
, и такие они могут значительно улучшить работу нашего приложения.
Наконец, всегда полезно использовать последнюю версию React, поскольку команда React постоянно работает над улучшением производительности и добавлением новых функций, которые могут помочь разработчикам оптимизировать свои приложения.