Основные способы предотвращения лишних рендеров в React:
Пример использования React.memo:
// Компонент перерисовывается только при изменении name
const UserGreeting = React.memo(function UserGreeting({ name }) {
console.log("UserGreeting рендерится");
return <h1>Привет, {name}!</h1>;
});
function App() {
const [name, setName] = useState("Мария");
const [counter, setCounter] = useState(0);
return (
<div>
<UserGreeting name={name} />
<button onClick={() => setName("Алексей")}>
Изменить имя
</button>
<button onClick={() => setCounter(c => c + 1)}>
Счетчик: {counter}
</button>
</div>
);
}Предотвращение ненужных перерисовок — один из ключевых аспектов оптимизации производительности React-приложений. React по умолчанию перерисовывает компоненты даже когда это не всегда необходимо, поэтому важно знать, как это контролировать. 🚀
React.memo — это HOC (компонент высшего порядка), который мемоизирует компонент, предотвращая его перерисовку, если пропсы не изменились:
const MovieCard = React.memo(function MovieCard({ title, rating }) {
console.log(`Рендер: ${title}`);
return (
<div className="movie-card">
<h3>{title}</h3>
<span>Рейтинг: {rating}/10</span>
</div>
);
});
// Использование
function MovieList() {
const [movies, setMovies] = useState(initialMovies);
const [filter, setFilter] = useState("");
// MovieCard перерисовывается только при изменении конкретного фильма
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Фильтр"
/>
{movies.map(movie => (
<MovieCard
key={movie.id}
title={movie.title}
rating={movie.rating}
/>
))}
</div>
);
}Можно настроить собственную логику сравнения пропсов:
const ProjectItem = React.memo(
function ProjectItem({ project, onSelect }) {
return (
<div onClick={() => onSelect(project.id)}>
{project.name}
</div>
);
},
(prevProps, nextProps) => {
// Рендер происходит только при изменении id или имени
return (
prevProps.project.id === nextProps.project.id &&
prevProps.project.name === nextProps.project.name
);
}
);useMemo позволяет кэшировать результаты дорогостоящих вычислений между рендерами:
function ProductList({ products, category }) {
// Фильтрация выполняется только при изменении products или category
const filteredProducts = useMemo(() => {
console.log("Фильтрация продуктов");
return products.filter(p => p.category === category);
}, [products, category]);
return (
<ul>
{filteredProducts.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}useCallback предотвращает создание новых функций при каждом рендере:
function TodoList() {
const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState("");
// Функция не пересоздаётся при каждом рендере
const handleDelete = useCallback((id) => {
setTodos(todos => todos.filter(todo => todo.id !== id));
}, []); // Зависимости пусты, так как используем функциональное обновление
return (
<div>
<input
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
/>
<button onClick={() => {
if (newTodo) {
setTodos([...todos, { id: Date.now(), text: newTodo }]);
setNewTodo("");
}
}}>
Добавить
</button>
{todos.map(todo => (
// TodoItem не будет перерисовываться, если меняется только текст в input
<TodoItem
key={todo.id}
todo={todo}
onDelete={handleDelete}
/>
))}
</div>
);
}
const TodoItem = React.memo(function TodoItem({ todo, onDelete }) {
console.log(`Рендер: ${todo.text}`);
return (
<div>
{todo.text}
<button onClick={() => onDelete(todo.id)}>Удалить</button>
</div>
);
});PureComponent — аналог React.memo для классовых компонентов:
class UserProfile extends React.PureComponent {
render() {
console.log("UserProfile рендерится");
const { name, email } = this.props;
return (
<div className="profile">
<h2>{name}</h2>
<p>{email}</p>
</div>
);
}
}Разделение компонентов по частоте изменений:
function SearchPage() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
// Часто меняющийся компонент (при каждом вводе)
return (
<div>
<SearchBar
query={query}
onChange={setQuery}
/>
{/* Редко меняющийся компонент (только при новых результатах) */}
<SearchResults results={results} />
</div>
);
}
// Оптимизированные компоненты
const SearchBar = React.memo(function SearchBar({ query, onChange }) {
return (
<input
value={query}
onChange={(e) => onChange(e.target.value)}
placeholder="Поиск"
/>
);
});
const SearchResults = React.memo(function SearchResults({ results }) {
return (
<ul>
{results.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
);
});Предотвращение создания новых объектов при каждом рендере:
// ❌ Плохо: новый объект стилей при каждом рендере
function Button({ primary }) {
return (
<button style={{
color: primary ? 'white' : 'black',
background: primary ? 'blue' : 'gray'
}}>
Нажми меня
</button>
);
}
// ✅ Хорошо: мемоизированные стили
function Button({ primary }) {
const buttonStyle = useMemo(() => ({
color: primary ? 'white' : 'black',
background: primary ? 'blue' : 'gray'
}), [primary]);
return <button style={buttonStyle}>Нажми меня</button>;
}
// Альтернативно: статические объекты
const primaryStyle = { color: 'white', background: 'blue' };
const secondaryStyle = { color: 'black', background: 'gray' };
function Button({ primary }) {
return (
<button style={primary ? primaryStyle : secondaryStyle}>
Нажми меня
</button>
);
}Разделение контекста на более мелкие части:
// ❌ Плохо: один большой контекст
const AppContext = React.createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
return (
<AppContext.Provider value={{
user, setUser,
theme, setTheme,
notifications, setNotifications
}}>
{children}
</AppContext.Provider>
);
}
// ✅ Хорошо: разделенные контексты
const UserContext = React.createContext();
const ThemeContext = React.createContext();
const NotificationContext = React.createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
<NotificationContext.Provider value={{ notifications, setNotifications }}>
{children}
</NotificationContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}Для приложений использующих Redux:
import { createSelector } from 'reselect';
// Базовые селекторы
const getUsers = state => state.users;
const getFilter = state => state.filter;
// Мемоизированный селектор
const getFilteredUsers = createSelector(
[getUsers, getFilter],
(users, filter) => {
console.log('Фильтрация пользователей');
return users.filter(user =>
user.name.toLowerCase().includes(filter.toLowerCase())
);
}
);
// Использование в компоненте
function UserList() {
const filteredUsers = useSelector(getFilteredUsers);
// Фильтрация выполняется только при изменении users или filter
return (
<ul>
{filteredUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}Профилирование для выявления лишних рендеров:
// Добавьте <React.Profiler> в ключевых местах
function App() {
const handleRender = (id, phase, actualDuration) => {
console.log(`Компонент ${id} рендерился за ${actualDuration}ms`);
};
return (
<React.Profiler id="Navigation" onRender={handleRender}>
<Navigation />
</React.Profiler>
);
}Автоматически показывает предотвратимые перерендеры:
// Setup в файле wdyr.js
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}
// Использование в компоненте
function ExampleComponent(props) {
// ... код компонента
}
ExampleComponent.whyDidYouRender = true;// ❌ Слишком рано: оптимизация простых компонентов
const Text = React.memo(function Text({ content }) {
return <p>{content}</p>;
});
// ✅ Правильно: оптимизируйте только компоненты со сложной логикой или глубоким деревом
const ComplexChart = React.memo(function ComplexChart({ data }) {
// Сложные вычисления и большое дерево компонентов
return <ChartComponent data={data} />;
});// ❌ Неправильно: забыты зависимости
function UserList({ users, onUserSelect }) {
const handleClick = useCallback((userId) => {
console.log(`Selected: ${userId}`);
onUserSelect(userId);
}, []); // Зависимость onUserSelect отсутствует
// ... код компонента
}
// ✅ Правильно: все зависимости включены
function UserList({ users, onUserSelect }) {
const handleClick = useCallback((userId) => {
console.log(`Selected: ${userId}`);
onUserSelect(userId);
}, [onUserSelect]);
// ... код компонента
}// ❌ Неэффективно: объект создается при каждом рендере родителя
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Счетчик: {count}
</button>
<MemoizedChild
data={{ value: 42 }} // Новый объект при каждом рендере
/>
</div>
);
}
const MemoizedChild = React.memo(function Child({ data }) {
console.log("Child рендерится несмотря на memo");
return <div>{data.value}</div>;
});
// ✅ Эффективно: стабильная ссылка на объект
function Parent() {
const [count, setCount] = useState(0);
// Объект создается один раз
const data = useMemo(() => ({ value: 42 }), []);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Счетчик: {count}
</button>
<MemoizedChild data={data} />
</div>
);
}✅ Основные техники предотвращения лишних рендеров:
React.memo для функциональных компонентовuseMemo для кэширования вычисляемых значенийuseCallback для стабильных функцийPureComponent для классовых компонентов✅ Рекомендации:
Правильное применение этих техник поможет существенно повысить производительность React-приложений, особенно при работе с большими списками данных или сложными интерфейсами. 🚀
Хотите больше статей для подготовки к собеседованиям? Подписывайтесь на EasyAdvice, добавляйте сайт в закладки и совершенствуйтесь каждый день 💪