Как устранить лишние перерендеры?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как устранить лишние перерендеры в React
Это критично для производительности. Лишние перерендеры — одна из главных причин медленных приложений. Давайте разберемся, как их найти и устранить.
Диагностика: найти лишние перерендеры
Способ 1: React DevTools Profiler
- Открыть DevTools -> Profiler вкладка
- Нажать кнопку записи
- Выполнить действие на сайте
- Остановить запись
- Посмотреть, какие компоненты рендерились и когда
**Способ 2: console.log
function MyComponent({ prop }) {
console.log("MyComponent рендерится", prop);
return <div>{prop}</div>;
}
Когда видишь много логов — значит перерендеров много.
Проблема 1: Функция создается каждый раз
Проблема:
function Parent() {
const handleClick = () => console.log("Клик"); // Новая функция каждый раз!
return (
<Child onClick={handleClick} />
);
}
const Child = memo(({ onClick }) => {
console.log("Child рендерится");
return <button onClick={onClick}>Клик</button>;
});
// Каждый раз Parent перерендеривается -> новая handleClick -> memo видит новую функцию -> Child перерендеривается
Решение: useCallback
function Parent() {
const handleClick = useCallback(() => {
console.log("Клик");
}, []); // Функция создается один раз
return <Child onClick={handleClick} />;
}
Проблема 2: Объект создается каждый раз
Проблема:
function Parent() {
const user = { name: "Иван", age: 30 }; // Новый объект каждый раз!
return <UserCard user={user} />;
}
const UserCard = memo(({ user }) => {
console.log("UserCard рендерится");
return <div>{user.name}</div>;
});
// Хотя user.name не изменился, объект user — новый -> memo видит изменение -> перерендер
**Решение: useMemo
function Parent() {
const user = useMemo(() => (
{ name: "Иван", age: 30 }
), []); // Объект создается один раз
return <UserCard user={user} />;
}
Проблема 3: State в неправильном месте
Проблема:
function App() {
const [query, setQuery] = useState("");
const [filters, setFilters] = useState({});
const [results, setResults] = useState([]);
// Когда меняется query -> App перерендеривается -> SearchBox, FilterPanel, ResultsList — все перерендеривются
// Но FilterPanel и ResultsList не используют query!
return (
<div>
<SearchBox value={query} onChange={setQuery} />
<FilterPanel filters={filters} onChange={setFilters} />
<ResultsList results={results} />
</div>
);
}
Решение: поднять state ближе к использованию
function SearchSection() {
const [query, setQuery] = useState(""); // State здесь, не в App
return <SearchBox value={query} onChange={setQuery} />;
}
function FilterSection() {
const [filters, setFilters] = useState({});
return <FilterPanel filters={filters} onChange={setFilters} />;
}
function ResultsSection({ results }) {
return <ResultsList results={results} />;
}
function App() {
const [results, setResults] = useState([]);
return (
<div>
<SearchSection /> {/* Перерендеривается только когда query меняется */}
<FilterSection /> {/* Независимо от query */}
<ResultsSection results={results} /> {/* Независимо от query */}
</div>
);
}
Проблема 4: Меморизация не работает
Проблема:
const UserCard = memo(({ user }) => {
console.log("UserCard рендерится");
return <div>{user.name}</div>;
});
function Parent() {
// user.name не меняется, но объект user новый
const user = { name: "Иван" };
return <UserCard user={user} />; // memo НЕ поможет!
}
Решение:
function Parent() {
const user = useMemo(() => ({ name: "Иван" }), []);
return <UserCard user={user} />; // Теперь memo сработает
}
ИЛИ сделать компонент более гибким:
const UserCard = memo(({ name }) => { // Принимаем примитив вместо объекта
console.log("UserCard рендерится");
return <div>{name}</div>;
});
function Parent() {
const name = "Иван"; // Примитив, меморизация не нужна
return <UserCard name={name} />;
}
Проблема 5: Context запускает перерендеры
Проблема:
const ThemeContext = createContext();
function App() {
const [theme, setTheme] = useState("light");
const [user, setUser] = useState(null);
// Когда user меняется -> App перерендеривается -> value новое -> все потребители Context перерендеривались
return (
<ThemeContext.Provider value={{ theme, setTheme, user, setUser }}>
<Page />
</ThemeContext.Provider>
);
}
function Header() {
const { theme } = useContext(ThemeContext); // Используем только theme
return <div className={theme}>...</div>;
}
// Когда user меняется -> Header перерендеривается (хотя theme не поменялся)
Решение 1: разделить Contexts
const ThemeContext = createContext();
const UserContext = createContext();
function App() {
const [theme, setTheme] = useState("light");
const [user, setUser] = useState(null);
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={user}>
<Page />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
// Теперь Header зависит только от ThemeContext
// Когда user меняется -> Header НЕ перерендеривается
**Решение 2: useMemo для value
function App() {
const [theme, setTheme] = useState("light");
const [user, setUser] = useState(null);
const themeValue = useMemo(() => ({ theme, setTheme }), [theme]);
const userValue = useMemo(() => ({ user, setUser }), [user]);
return (
<ThemeContext.Provider value={themeValue}>
<UserContext.Provider value={userValue}>
<Page />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
Проблема 6: Списки без правильных ключей
Проблема:
// ПЛОХО: индекс как ключ
list.map((item, index) => <Item key={index} item={item} />)
// Если удалить первый элемент:
// list[0] теперь другой элемент
// Item с key=0 будет показывать новый элемент (вместо перезагрузки)
// Это может привести к странным багам и лишним перерендерам
Решение:
// ХОРОШО: уникальный ID
list.map(item => <Item key={item.id} item={item} />)
Проблема 7: Логика в render функции
Проблема:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// Эта логика выполняется при КАЖДОМ рендере!
const fetchUser = async () => {
const res = await fetch(`/api/users/${userId}`);
setUser(await res.json());
};
return <div>{user?.name}</div>;
}
**Решение: useEffect
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// Выполняется только когда userId меняется
useEffect(() => {
const fetchUser = async () => {
const res = await fetch(`/api/users/${userId}`);
setUser(await res.json());
};
fetchUser();
}, [userId]);
return <div>{user?.name}</div>;
}
Инструмент: why-did-you-render
import whyDidYouRender from '@welldone-software/why-did-you-render';
if (process.env.NODE_ENV === 'development') {
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}
Теперь в консоли видишь точную причину перерендера каждого компонента.
Чеклист оптимизации
- Профилировать — найти реальную проблему
- useCallback для колбэков — если они переданы в memo() компоненты
- useMemo для объектов/массивов — если они переданы в memo() компоненты
- Разделить state — не держать всё в одном месте
- Разделить Contexts — если у тебя много значений
- Правильные ключи в списках — всегда уникальные ID
- Логика в useEffect — не в render функции
- Избегать inline объектов/функций в props
Пример оптимизированного компонента
const OptimizedChild = memo(({ items, onAdd }) => {
console.log("OptimizedChild рендерится");
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
<button onClick={onAdd}>Добавить</button>
</ul>
);
});
function Parent() {
const [items, setItems] = useState([]);
const [count, setCount] = useState(0);
// items меморизирован
const memoItems = useMemo(() => items, [items]);
// onAdd меморизирован
const onAdd = useCallback(() => {
setItems(prev => [...prev, { id: Date.now(), name: "Новый" }]);
}, []);
return (
<div>
<button onClick={() => setCount(count + 1)}>Счетчик: {count}</button>
<OptimizedChild items={memoItems} onAdd={onAdd} />
{/* Когда count меняется -> Parent перерендеривается -> OptimizedChild НЕ перерендеривается */}
</div>
);
}
Главное — профилировать сначала, оптимизировать потом. Большинство приложений работают достаточно быстро без излишней оптимизации.