← Назад к вопросам

Как сделать минимальное количество рендеров при получении карточек на странице?

2.0 Middle🔥 121 комментариев
#React

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Проблема: Избыточные рендеры

При отображении списка карточек компонент может переrendериться много раз без необходимости. Каждый ненужный рендер замедляет приложение, особенно при большом количестве карточек.

Способ 1: Мемоизация компонента (React.memo)

// Неправильно: каждая карточка перерендеривается
function CardItem({ card, onLike }) {
  return (
    <div className="card">
      <h3>{card.title}</h3>
      <button onClick={() => onLike(card.id)}>Like</button>
    </div>
  );
}

// Правильно: завернуть в React.memo
const CardItem = React.memo(function CardItem({ card, onLike }) {
  console.log(`Rendering card ${card.id}`); // Лог для отладки
  
  return (
    <div className="card">
      <h3>{card.title}</h3>
      <button onClick={() => onLike(card.id)}>Like</button>
    </div>
  );
});

// React.memo сравнивает props и перерендеривает только если они изменились

Способ 2: useCallback для стабильных функций

// Неправильно: функция создаётся заново при каждом рендере
function CardsList({ cards }) {
  const onLike = (cardId) => {
    console.log(`Liked card ${cardId}`);
  };
  // onLike новая -> CardItem всегда перерендеривается!
  
  return (
    <div>
      {cards.map(card => (
        <CardItem key={card.id} card={card} onLike={onLike} />
      ))}
    </div>
  );
}

// Правильно: useCallback для сохранения функции
function CardsList({ cards }) {
  const onLike = useCallback((cardId) => {
    console.log(`Liked card ${cardId}`);
  }, []); // Зависимости пусты, функция создаётся один раз
  
  return (
    <div>
      {cards.map(card => (
        <CardItem key={card.id} card={card} onLike={onLike} />
      ))}
    </div>
  );
}

// Если функция зависит от переменных:
function CardsList({ cards, userId }) {
  const onLike = useCallback((cardId) => {
    fetch(`/api/users/${userId}/likes/${cardId}`, {
      method: "POST"
    });
  }, [userId]); // Функция пересоздаётся только если userId изменился
  
  return (
    <div>
      {cards.map(card => (
        <CardItem key={card.id} card={card} onLike={onLike} />
      ))}
    </div>
  );
}

Способ 3: Правильные ключи и виртуализация

// Неправильно: индекс как key
function CardsList({ cards }) {
  return (
    <div>
      {cards.map((card, index) => (
        <CardItem key={index} card={card} />
        // При добавлении элемента индексы сдвигаются, перерендеры везде!
      ))}
    </div>
  );
}

// Правильно: уникальный id
function CardsList({ cards }) {
  return (
    <div>
      {cards.map(card => (
        <CardItem key={card.id} card={card} />
      ))}
    </div>
  );
}

// Виртуализация для больших списков:
import { FixedSizeList as List } from "react-window";

function VirtualizedCardsList({ cards }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      <CardItem card={cards[index]} />
    </div>
  );
  
  return (
    <List
      height={600}
      itemCount={cards.length}
      itemSize={200}
      width="100%"
    >
      {Row}
    </List>
  );
  // Рендеритсялось только видимые элементы вместо всех
}

Способ 4: useMemo для дорогостоящих вычислений

// Неправильно: фильтр пересчитывается каждый рендер
function CardsList({ cards, filterText }) {
  const filteredCards = cards.filter(card =>
    card.title.toLowerCase().includes(filterText.toLowerCase())
  );
  
  return (
    <div>
      {filteredCards.map(card => (
        <CardItem key={card.id} card={card} />
      ))}
    </div>
  );
}

// Правильно: useMemo
function CardsList({ cards, filterText }) {
  const filteredCards = useMemo(() => {
    return cards.filter(card =>
      card.title.toLowerCase().includes(filterText.toLowerCase())
    );
  }, [cards, filterText]); // Пересчитываем только если зависимости изменились
  
  return (
    <div>
      {filteredCards.map(card => (
        <CardItem key={card.id} card={card} />
      ))}
    </div>
  );
}

Способ 5: Комбинированный пример

// CardItem.jsx - мемоизированный
const CardItem = React.memo(function CardItem({ card, onLike }) {
  return (
    <div className="card">
      <h3>{card.title}</h3>
      <button onClick={() => onLike(card.id)}>Like</button>
    </div>
  );
});

// CardsList.jsx - оптимизированный список
function CardsList({ initialCards }) {
  const [cards, setCards] = useState(initialCards);
  const [filterText, setFilterText] = useState("");
  
  const onLike = useCallback((cardId) => {
    setCards(cards.map(card =>
      card.id === cardId ? { ...card, likes: card.likes + 1 } : card
    ));
  }, [cards]);
  
  const filteredCards = useMemo(() => {
    return cards.filter(card =>
      card.title.toLowerCase().includes(filterText.toLowerCase())
    );
  }, [cards, filterText]);
  
  return (
    <div>
      <input
        type="text"
        placeholder="Filter cards..."
        value={filterText}
        onChange={(e) => setFilterText(e.target.value)}
      />
      <div className="cards-container">
        {filteredCards.map(card => (
          <CardItem key={card.id} card={card} onLike={onLike} />
        ))}
      </div>
    </div>
  );
}

Способ 6: Ленивая загрузка (Infinite Scroll)

function InfiniteCardsList() {
  const [cards, setCards] = useState([]);
  const [page, setPage] = useState(0);
  const [loading, setLoading] = useState(false);
  const observerTarget = useRef(null);
  
  const loadMore = useCallback(async () => {
    if (loading) return;
    
    setLoading(true);
    try {
      const response = await fetch(`/api/cards?page=${page}&limit=20`);
      const newCards = await response.json();
      setCards(prev => [...prev, ...newCards]);
      setPage(prev => prev + 1);
    } finally {
      setLoading(false);
    }
  }, [page, loading]);
  
  useEffect(() => {
    const observer = new IntersectionObserver(
      entries => {
        if (entries[0].isIntersecting && !loading) {
          loadMore();
        }
      },
      { threshold: 0.5 }
    );
    
    if (observerTarget.current) {
      observer.observe(observerTarget.current);
    }
    
    return () => {
      if (observerTarget.current) {
        observer.unobserve(observerTarget.current);
      }
    };
  }, [loading, loadMore]);
  
  return (
    <div>
      <div className="cards-grid">
        {cards.map(card => (
          <CardItem key={card.id} card={card} />
        ))}
      </div>
      <div ref={observerTarget}>
        {loading && <p>Loading...</p>}
      </div>
    </div>
  );
}

Чеклист оптимизации

  1. Используй React.memo для компонентов
  2. Используй useCallback для функций, передаваемых как props
  3. Используй useMemo для дорогостоящих вычислений
  4. Используй правильные ключи (уникальные id, не индексы)
  5. Рассмотри виртуализацию для больших списков
  6. Рассмотри ленивую загрузку (infinite scroll)
  7. Избегай inline объектов и функций в render
Как сделать минимальное количество рендеров при получении карточек на странице? | PrepBro