Посібник з оптимізації продуктивності React за допомогою useMemo, useCallback та React.memo. Запобігайте зайвим перерендерам і покращуйте досвід користувача.
Оптимізація продуктивності React: опановуємо useMemo, useCallback та React.memo
React, популярна JavaScript-бібліотека для створення інтерфейсів користувача, відома своєю компонентною архітектурою та декларативним стилем. Однак, у міру зростання складності застосунків, продуктивність може стати проблемою. Непотрібні перерендери компонентів можуть призвести до уповільнення роботи та поганого досвіду користувача. На щастя, React надає кілька інструментів для оптимізації продуктивності, зокрема useMemo
, useCallback
та React.memo
. Цей посібник детально розглядає ці техніки, надаючи практичні приклади та корисні поради, які допоможуть вам створювати високопродуктивні React-застосунки.
Розуміння перерендерів у React
Перш ніж заглиблюватися в техніки оптимізації, важливо зрозуміти, чому в React відбуваються перерендери. Коли змінюється стан або пропси компонента, React ініціює перерендер цього компонента та, потенційно, його дочірніх компонентів. React використовує віртуальний DOM для ефективного оновлення реального DOM, але надмірні перерендери все одно можуть впливати на продуктивність, особливо у складних застосунках. Уявіть собі глобальну e-commerce платформу, де ціни на товари часто оновлюються. Без оптимізації навіть невелика зміна ціни може викликати перерендери всього списку товарів, що вплине на досвід перегляду користувачем.
Чому компоненти перерендеряться
- Зміни стану: Коли стан компонента оновлюється за допомогою
useState
абоuseReducer
, React перерендерює компонент. - Зміни пропсів: Якщо компонент отримує нові пропси від свого батьківського компонента, він перерендерюється.
- Перерендери батьківського компонента: Коли батьківський компонент перерендерюється, його дочірні компоненти також перерендерюються за замовчуванням, незалежно від того, чи змінилися їхні пропси.
- Зміни контексту: Компоненти, які використовують React Context, перерендерюються, коли значення контексту змінюється.
Мета оптимізації продуктивності — запобігти непотрібним перерендерам, гарантуючи, що компоненти оновлюються лише тоді, коли їхні дані дійсно змінилися. Розглянемо сценарій, що включає візуалізацію даних у реальному часі для аналізу фондового ринку. Якщо компоненти діаграм перерендерюються без потреби при кожному незначному оновленні даних, застосунок стане невідгукливим. Оптимізація перерендерів забезпечить плавний та чутливий досвід користувача.
Представляємо useMemo: мемоізація дорогих обчислень
useMemo
— це хук React, який мемоізує результат обчислення. Мемоізація — це техніка оптимізації, яка зберігає результати дорогих викликів функцій і повторно використовує ці результати, коли ті ж самі вхідні дані з'являються знову. Це дозволяє уникнути непотрібного повторного виконання функції.
Коли використовувати useMemo
- Дорогі обчислення: Коли компоненту потрібно виконати обчислювально інтенсивну операцію на основі його пропсів або стану.
- Посилальна рівність: Коли ви передаєте значення як пропс дочірньому компоненту, який покладається на посилальну рівність для визначення необхідності перерендеру.
Як працює useMemo
useMemo
приймає два аргументи:
- Функція, яка виконує обчислення.
- Масив залежностей.
Функція виконується лише тоді, коли змінюється одна із залежностей у масиві. В іншому випадку useMemo
повертає раніше мемоізоване значення.
Приклад: обчислення послідовності Фібоначчі
Послідовність Фібоначчі — це класичний приклад обчислювально інтенсивної операції. Створімо компонент, який обчислює n-те число Фібоначчі за допомогою useMemo
.
import React, { useState, useMemo } from 'react';
function Fibonacci({ n }) {
const fibonacciNumber = useMemo(() => {
console.log('Обчислення Фібоначчі...'); // Демонструє, коли запускається обчислення
function calculateFibonacci(num) {
if (num <= 1) {
return num;
}
return calculateFibonacci(num - 1) + calculateFibonacci(num - 2);
}
return calculateFibonacci(n);
}, [n]);
return Fibonacci({n}) = {fibonacciNumber}
;
}
function App() {
const [number, setNumber] = useState(5);
return (
setNumber(parseInt(e.target.value))}
/>
);
}
export default App;
У цьому прикладі функція calculateFibonacci
виконується лише тоді, коли змінюється пропс n
. Без useMemo
функція виконувалася б при кожному перерендері компонента Fibonacci
, навіть якби n
залишалося незмінним. Уявіть, що це обчислення відбувається на глобальній фінансовій панелі - кожна зміна на ринку викликає повний перерахунок, що призводить до значних затримок. useMemo
запобігає цьому.
Представляємо useCallback: мемоізація функцій
useCallback
— це ще один хук React, який мемоізує функції. Він запобігає створенню нового екземпляра функції при кожному рендері, що може бути особливо корисним при передачі колбеків як пропсів дочірнім компонентам.
Коли використовувати useCallback
- Передача колбеків як пропсів: При передачі функції як пропса дочірньому компоненту, який використовує
React.memo
абоshouldComponentUpdate
для оптимізації перерендерів. - Обробники подій: При визначенні функцій-обробників подій у компоненті для запобігання непотрібним перерендерам дочірніх компонентів.
Як працює useCallback
useCallback
приймає два аргументи:
- Функція, яку потрібно мемоізувати.
- Масив залежностей.
Функція створюється заново лише тоді, коли змінюється одна із залежностей у масиві. В іншому випадку useCallback
повертає той самий екземпляр функції.
Приклад: обробка кліку на кнопку
Створімо компонент із кнопкою, яка викликає функцію-колбек. Ми використаємо useCallback
для мемоізації цієї функції.
import React, { useState, useCallback } from 'react';
function Button({ onClick, children }) {
console.log('Кнопка перерендерюється'); // Демонструє, коли кнопка перерендерюється
return ;
}
const MemoizedButton = React.memo(Button);
function App() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('Кнопку натиснуто');
setCount((prevCount) => prevCount + 1);
}, []); // Порожній масив залежностей означає, що функція створюється лише один раз
return (
Count: {count}
Increment
);
}
export default App;
У цьому прикладі функція handleClick
створюється лише один раз, оскільки масив залежностей порожній. Коли компонент App
перерендерюється через зміну стану count
, функція handleClick
залишається тією ж самою. Компонент MemoizedButton
, обгорнутий у React.memo
, перерендерюватиметься лише у випадку зміни його пропсів. Оскільки пропс onClick
(handleClick
) залишається незмінним, компонент Button
не перерендерюється без потреби. Уявіть собі інтерактивну карту. Кожного разу, коли користувач взаємодіє з нею, це може впливати на десятки компонентів кнопок. Без useCallback
ці кнопки непотрібно перерендерюватимуться, що створює затримки. Використання useCallback
забезпечує більш плавну взаємодію.
Представляємо React.memo: мемоізація компонентів
React.memo
— це компонент вищого порядку (HOC), який мемоізує функціональний компонент. Він запобігає перерендеру компонента, якщо його пропси не змінилися. Це схоже на PureComponent
для класових компонентів.
Коли використовувати React.memo
- Чисті компоненти: Коли результат роботи компонента залежить виключно від його пропсів, і він не має власного стану.
- Дорогий рендеринг: Коли процес рендерингу компонента є обчислювально дорогим.
- Часті перерендери: Коли компонент часто перерендерюється, хоча його пропси не змінилися.
Як працює React.memo
React.memo
обгортає функціональний компонент і виконує поверхневе порівняння попередніх і наступних пропсів. Якщо пропси однакові, компонент не буде перерендерюватися.
Приклад: відображення профілю користувача
Створімо компонент, який відображає профіль користувача. Ми використаємо React.memo
, щоб запобігти непотрібним перерендерам, якщо дані користувача не змінилися.
import React from 'react';
function UserProfile({ user }) {
console.log('UserProfile перерендерюється'); // Демонструє, коли компонент перерендерюється
return (
Name: {user.name}
Email: {user.email}
);
}
const MemoizedUserProfile = React.memo(UserProfile, (prevProps, nextProps) => {
// Кастомна функція порівняння (необов'язково)
return prevProps.user.id === nextProps.user.id; // Перерендерювати, лише якщо змінюється ID користувача
});
function App() {
const [user, setUser] = React.useState({
id: 1,
name: 'John Doe',
email: 'john.doe@example.com',
});
const updateUser = () => {
setUser({ ...user, name: 'Jane Doe' }); // Зміна імені
};
return (
);
}
export default App;
У цьому прикладі компонент MemoizedUserProfile
буде перерендерюватися лише тоді, коли змінюється пропс user.id
. Навіть якщо інші властивості об'єкта user
змінюються (наприклад, ім'я або електронна пошта), компонент не буде перерендерюватися, доки ID не стане іншим. Ця кастомна функція порівняння в `React.memo` дозволяє точно контролювати, коли компонент перерендерюється. Уявіть собі платформу соціальної мережі з профілями користувачів, що постійно оновлюються. Без `React.memo` зміна статусу або фотографії профілю користувача викликала б повний перерендер компонента профілю, навіть якщо основні дані користувача залишаються незмінними. `React.memo` дозволяє робити цільові оновлення та значно покращує продуктивність.
Поєднання useMemo, useCallback та React.memo
Ці три техніки є найефективнішими, коли використовуються разом. useMemo
мемоізує дорогі обчислення, useCallback
мемоізує функції, а React.memo
мемоізує компоненти. Поєднуючи ці техніки, ви можете значно зменшити кількість непотрібних перерендерів у вашому React-застосунку.
Приклад: складний компонент
Створімо більш складний компонент, який демонструє, як поєднувати ці техніки.
import React, { useState, useCallback, useMemo } from 'react';
function ListItem({ item, onUpdate, onDelete }) {
console.log(`ListItem ${item.id} перерендерюється`); // Демонструє, коли компонент перерендерюється
return (
{item.text}
);
}
const MemoizedListItem = React.memo(ListItem);
function List({ items, onUpdate, onDelete }) {
console.log('List перерендерюється'); // Демонструє, коли компонент перерендерюється
return (
{items.map((item) => (
))}
);
}
const MemoizedList = React.memo(List);
function App() {
const [items, setItems] = useState([
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' },
]);
const handleUpdate = useCallback((id) => {
setItems((prevItems) =>
prevItems.map((item) =>
item.id === id ? { ...item, text: `Updated ${item.text}` } : item
)
);
}, []);
const handleDelete = useCallback((id) => {
setItems((prevItems) => prevItems.filter((item) => item.id !== id));
}, []);
const memoizedItems = useMemo(() => items, [items]);
return (
);
}
export default App;
У цьому прикладі:
useCallback
використовується для мемоізації функційhandleUpdate
таhandleDelete
, що запобігає їх повторному створенню при кожному рендері.useMemo
використовується для мемоізації масивуitems
, що запобігає перерендеру компонентаList
, якщо посилання на масив не змінилося.React.memo
використовується для мемоізації компонентівListItem
таList
, що запобігає їх перерендеру, якщо їхні пропси не змінилися.
Таке поєднання технік гарантує, що компоненти перерендерюються лише за необхідності, що призводить до значного підвищення продуктивності. Уявіть собі масштабний інструмент для управління проєктами, де списки завдань постійно оновлюються, видаляються та перевпорядковуються. Без цих оптимізацій будь-яка невелика зміна у списку завдань викликала б каскад перерендерів, роблячи застосунок повільним і невідгукливим. Стратегічне використання useMemo
, useCallback
та React.memo
дозволяє застосунку залишатися продуктивним навіть при роботі зі складними даними та частими оновленнями.
Додаткові техніки оптимізації
Хоча useMemo
, useCallback
та React.memo
є потужними інструментами, вони не єдині способи оптимізації продуктивності React. Ось ще кілька технік, які варто розглянути:
- Розділення коду (Code Splitting): Розбийте ваш застосунок на менші частини (чанки), які можна завантажувати за вимогою. Це зменшує час початкового завантаження та покращує загальну продуктивність.
- Ліниве завантаження (Lazy Loading): Завантажуйте компоненти та ресурси лише тоді, коли вони потрібні. Це може бути особливо корисним для зображень та інших великих ресурсів.
- Віртуалізація: Рендеріть лише видиму частину великого списку або таблиці. Це може значно покращити продуктивність при роботі з великими наборами даних. У цьому можуть допомогти бібліотеки, такі як
react-window
таreact-virtualized
. - Debouncing та Throttling: Обмежте частоту виконання функцій. Це може бути корисним для обробки таких подій, як прокрутка та зміна розміру вікна.
- Імутабельність: Використовуйте імутабельні структури даних, щоб уникнути випадкових мутацій та спростити виявлення змін.
Глобальні аспекти оптимізації
При оптимізації React-застосунків для глобальної аудиторії важливо враховувати такі фактори, як затримка мережі, можливості пристроїв та локалізація. Ось кілька порад:
- Мережі доставки контенту (CDN): Використовуйте CDN для роздачі статичних ресурсів з місць, розташованих ближче до ваших користувачів. Це зменшує затримку мережі та покращує час завантаження.
- Оптимізація зображень: Оптимізуйте зображення для різних розмірів екранів та роздільних здатностей. Використовуйте техніки стиснення для зменшення розміру файлів.
- Локалізація: Завантажуйте лише необхідні мовні ресурси для кожного користувача. Це зменшує час початкового завантаження та покращує досвід користувача.
- Адаптивне завантаження: Визначайте мережеве з'єднання та можливості пристрою користувача та відповідно коригуйте поведінку застосунку. Наприклад, ви можете вимкнути анімації або зменшити якість зображень для користувачів з повільним інтернет-з'єднанням або на старих пристроях.
Висновок
Оптимізація продуктивності React-застосунків є ключовою для забезпечення плавного та чутливого досвіду користувача. Опанувавши такі техніки, як useMemo
, useCallback
та React.memo
, а також враховуючи глобальні стратегії оптимізації, ви зможете створювати високопродуктивні React-застосунки, які масштабуються для задоволення потреб різноманітної бази користувачів. Не забувайте профілювати ваш застосунок, щоб виявити вузькі місця у продуктивності та застосовувати ці техніки оптимізації стратегічно. Не оптимізуйте передчасно – зосередьтеся на тих областях, де ви можете досягти найзначнішого ефекту.
Цей посібник надає міцну основу для розуміння та впровадження оптимізацій продуктивності в React. As you continue to develop React applications, remember to prioritize performance and continuously seek out new ways to improve the user experience.