← Назад к вопросам
Как оптимизируешь рендер на React частичной выдачи карточек пользователю?
2.0 Middle🔥 201 комментариев
#React#Архитектура и паттерны
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Оптимизация рендера карточек в React
Привет! Вопрос про оптимизацию рендера при выводе большого количества карточек (списки пользователей, товаров, постов). Это классическая задача, где наивный подход быстро приведет к lag.
Проблема: рендер 1000+ карточек
Если рендерить всё сразу:
// Плохо - рендерит все 1000 карточек одновременно
function UsersList({ users }) {
return (
<div>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}
Что происходит:
- React создает 1000 компонентов в памяти
- DOM операции для всех 1000 элементов
- Браузер зависает при прокрутке
- Первое отображение может занять секунды
Решение 1: Виртуализация (Virtual Scrolling)
Рендерим только видимые элементы + буфер:
// Вариант 1: Использование react-window (популярная библиотека)
import { FixedSizeList as List } from 'react-window';
function VirtualizedUsersList({ users }) {
const Row = ({ index, style }) => (
<div style={style}>
<UserCard user={users[index]} />
</div>
);
return (
<List
height={600}
itemCount={users.length}
itemSize={100} // высота одной карточки
width="100%"
>
{Row}
</List>
);
}
// Вариант 2: Собственная реализация с Intersection Observer
function CustomVirtualList({ users, itemHeight = 100 }) {
const [visibleRange, setVisibleRange] = useState({ start: 0, end: 10 });
const containerRef = useRef(null);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const handleScroll = () => {
const scrollTop = container.scrollTop;
const start = Math.floor(scrollTop / itemHeight);
const end = start + Math.ceil(container.clientHeight / itemHeight) + 5;
setVisibleRange({ start, end });
};
container.addEventListener('scroll', handleScroll);
return () => container.removeEventListener('scroll', handleScroll);
}, [itemHeight]);
const visibleUsers = users.slice(visibleRange.start, visibleRange.end);
const offsetY = visibleRange.start * itemHeight;
return (
<div
ref={containerRef}
style={{ height: '600px', overflowY: 'auto', position: 'relative' }}
>
<div style={{ height: users.length * itemHeight }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleUsers.map((user) => (
<UserCard key={user.id} user={user} />
))}
</div>
</div>
</div>
);
}
Решение 2: Ленивая загрузка (Lazy Loading)
Загружаем данные по мере скролла:
function LazyLoadingList() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const observerTarget = useRef(null);
const loadMore = useCallback(async () => {
if (loading || !hasMore) return;
setLoading(true);
try {
const response = await fetch(`/api/users?page=${page}`);
const newItems = await response.json();
setItems(prev => [...prev, ...newItems]);
setPage(prev => prev + 1);
setHasMore(newItems.length > 0);
} catch (error) {
console.error('Failed to load:', error);
} finally {
setLoading(false);
}
}, [page, loading, hasMore]);
// Intersection Observer для срабатывания при скролле
useEffect(() => {
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting && hasMore && !loading) {
loadMore();
}
},
{ threshold: 0.1 }
);
if (observerTarget.current) {
observer.observe(observerTarget.current);
}
return () => observer.disconnect();
}, [loadMore, hasMore, loading]);
return (
<div>
<div className="user-list">
{items.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
{hasMore && <div ref={observerTarget}>Loading...</div>}
</div>
);
}
Решение 3: Оптимизация рендера компонентов
Минимизируй перерендеры карточек:
// Используй React.memo для предотвращения ненужных перерендеров
const UserCard = React.memo(({ user, onClick }) => {
console.log('Rendering card:', user.id); // зависнет при скролле, если не мемоизировано
return (
<div className="card" onClick={onClick}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}, (prevProps, nextProps) => {
// Кастомная проверка равенства
return (
prevProps.user.id === nextProps.user.id &&
prevProps.onClick === nextProps.onClick
);
});
// Родитель должен мемоизировать callbacks
function UsersList({ users }) {
const handleCardClick = useCallback((userId) => {
console.log('Clicked:', userId);
}, []);
return (
<div>
{users.map(user => (
<UserCard
key={user.id}
user={user}
onClick={() => handleCardClick(user.id)}
/>
))}
</div>
);
}
Решение 4: Комбинированный подход (Лучше всего)
function OptimizedUsersList({ initialUsers }) {
const [items, setItems] = useState(initialUsers);
const [visibleRange, setVisibleRange] = useState({ start: 0, end: 20 });
const [isLoading, setIsLoading] = useState(false);
const containerRef = useRef(null);
const itemHeight = 120;
const visibleUsers = items.slice(visibleRange.start, visibleRange.end);
const offsetY = visibleRange.start * itemHeight;
const handleScroll = useCallback(() => {
const container = containerRef.current;
if (!container) return;
const scrollTop = container.scrollTop;
const start = Math.floor(scrollTop / itemHeight);
const end = start + Math.ceil(container.clientHeight / itemHeight) + 10;
setVisibleRange({ start, end });
// Загрузить больше, если приближаемся к концу
if (end >= items.length - 20 && !isLoading) {
loadMoreItems();
}
}, [items.length, isLoading]);
const loadMoreItems = useCallback(async () => {
setIsLoading(true);
try {
const response = await fetch(`/api/users?offset=${items.length}`);
const newItems = await response.json();
setItems(prev => [...prev, ...newItems]);
} finally {
setIsLoading(false);
}
}, [items.length]);
return (
<div
ref={containerRef}
style={{
height: '600px',
overflowY: 'auto',
border: '1px solid #ddd'
}}
onScroll={handleScroll}
>
<div style={{ height: items.length * itemHeight }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleUsers.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
</div>
{isLoading && <div style={{ padding: '20px' }}>Loading...</div>}
</div>
);
}
Решение 5: Pagination (для продуктовых сайтов)
Просто показываем страницы по 20-50 элементов:
function PaginatedUsersList() {
const [page, setPage] = useState(1);
const [users, setUsers] = useState([]);
const [totalPages, setTotalPages] = useState(0);
useEffect(() => {
const fetchUsers = async () => {
const response = await fetch(`/api/users?page=${page}&limit=20`);
const data = await response.json();
setUsers(data.items);
setTotalPages(data.totalPages);
};
fetchUsers();
}, [page]);
return (
<div>
<div className="users-grid">
{users.map(user => <UserCard key={user.id} user={user} />)}
</div>
<div className="pagination">
{Array.from({ length: totalPages }, (_, i) => i + 1).map(p => (
<button
key={p}
onClick={() => setPage(p)}
className={p === page ? 'active' : ''}
>
{p}
</button>
))}
</div>
</div>
);
}
Сравнение подходов
| Подход | Плюсы | Минусы | Когда использовать |
|---|---|---|---|
| Virtual Scrolling | Плавный скролл | Сложнее реализовать | 5000+ элементов |
| Lazy Loading | Экономит трафик | Нужен бэкенд | Много элементов |
| Pagination | Просто | Неудобна на мобилке | Средние списки |
| Мемоизация | Быстро | Требует внимания | Все подходы |
Best Practices
- Всегда используй key={id}, не index
- Мемоизируй callback'и через useCallback
- Не передавай объекты как props без мемоизации
- Измеряй производительность через DevTools
- Комбинируй подходы: virtual scrolling + lazy loading
- Кэшируй загруженные данные (useMemo, Redux, TanStack Query)
- Показывай скелетоны при загрузке
Итого
Для 1000+ карточек используй виртуализацию (react-window или собственная реализация). Комбинируй с ленивой загрузкой данных и мемоизацией компонентов. Не забывай про правильный key и useCallback. Это даст плавный скролл и быструю загрузку.