Виртуализация списков с react-window: как отображать тысячи элементов без лагов

16 марта 2026
Автор

Олег Марков

react-window — виртуализация списков

react-window — библиотека для виртуализации больших списков и таблиц. Вместо рендера всех элементов одновременно, она рендерит только видимые элементы в области прокрутки, что кардинально улучшает производительность при работе с тысячами строк.

npm install react-window

Проблема: рендер тысяч элементов

// ❌ Плохо — рендерим 10 000 элементов в DOM
function SlowList({ items }: { items: Item[] }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li> // 10 000 DOM-узлов!
      ))}
    </ul>
  );
}

// ✅ Хорошо — react-window рендерит только ~20 видимых элементов

Виртуализация рендерит только элементы, попадающие в видимую область экрана. При прокрутке элементы переиспользуются с обновлёнными данными.

FixedSizeList — список с одинаковой высотой строк

Используйте когда все строки имеют одинаковую высоту:

import { FixedSizeList as List } from 'react-window';

interface Item {
  id: number;
  name: string;
  email: string;
}

// Компонент строки — должен быть чистым и принимать style из данных
interface RowProps {
  index: number;
  style: React.CSSProperties; // Обязательно передавать в корневой элемент!
  data: Item[];
}

function Row({ index, style, data }: RowProps) {
  const item = data[index];
  return (
    <div style={style} className="list-row"> {/* style ОБЯЗАТЕЛЕН */}
      <span>{item.name}</span>
      <span>{item.email}</span>
    </div>
  );
}

function UserList({ users }: { users: Item[] }) {
  return (
    <List
      height={600}           // Высота контейнера списка
      itemCount={users.length} // Общее количество элементов
      itemSize={50}          // Высота каждой строки в пикселях
      width="100%"           // Ширина контейнера
      itemData={users}       // Данные, доступные в компоненте строки
    >
      {Row}
    </List>
  );
}

VariableSizeList — список с разной высотой строк

Когда строки имеют разную высоту, используйте VariableSizeList:

import { VariableSizeList as List } from 'react-window';

const ITEM_SIZES = [50, 80, 35, 120, 60]; // Разная высота для каждой строки

function getItemSize(index: number): number {
  // Логика определения высоты по индексу
  return ITEM_SIZES[index % ITEM_SIZES.length];
}

interface VariableRowProps {
  index: number;
  style: React.CSSProperties;
  data: Post[];
}

function PostRow({ index, style, data }: VariableRowProps) {
  const post = data[index];
  return (
    <div style={style} className="post-row">
      <h3>{post.title}</h3>
      {post.hasImage && <img src={post.thumbnail} alt="" />}
      <p>{post.excerpt}</p>
    </div>
  );
}

function PostList({ posts }: { posts: Post[] }) {
  return (
    <List
      height={600}
      itemCount={posts.length}
      itemSize={getItemSize}  // Функция вместо числа
      width="100%"
      itemData={posts}
    >
      {PostRow}
    </List>
  );
}

FixedSizeGrid — двумерная таблица

Для таблиц с фиксированными размерами ячеек:

import { FixedSizeGrid as Grid } from 'react-window';

interface CellProps {
  columnIndex: number;
  rowIndex: number;
  style: React.CSSProperties;
  data: string[][];
}

function Cell({ columnIndex, rowIndex, style, data }: CellProps) {
  return (
    <div style={style} className="grid-cell">
      {data[rowIndex][columnIndex]}
    </div>
  );
}

function DataGrid({ matrix }: { matrix: string[][] }) {
  return (
    <Grid
      columnCount={matrix[0].length}  // Количество столбцов
      columnWidth={150}               // Ширина столбца
      height={500}                    // Высота контейнера
      rowCount={matrix.length}        // Количество строк
      rowHeight={35}                  // Высота строки
      width={900}                     // Ширина контейнера
      itemData={matrix}
    >
      {Cell}
    </Grid>
  );
}

Автоматический размер с react-virtualized-auto-sizer

Для адаптации к размеру контейнера используйте AutoSizer:

npm install react-virtualized-auto-sizer
import { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';

function ResponsiveList({ items }: { items: Item[] }) {
  return (
    // AutoSizer занимает 100% родительского контейнера
    <div style={{ height: '100vh' }}>
      <AutoSizer>
        {({ height, width }) => (
          <FixedSizeList
            height={height}  // Динамическая высота
            width={width}    // Динамическая ширина
            itemCount={items.length}
            itemSize={50}
            itemData={items}
          >
            {Row}
          </FixedSizeList>
        )}
      </AutoSizer>
    </div>
  );
}

Бесконечная прокрутка

Загрузка данных по мере прокрутки:

import { FixedSizeList } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';

interface InfiniteListProps {
  hasNextPage: boolean;
  isNextPageLoading: boolean;
  items: Item[];
  loadNextPage: () => Promise<void>;
}

function InfiniteList({
  hasNextPage,
  isNextPageLoading,
  items,
  loadNextPage,
}: InfiniteListProps) {
  // Добавляем заглушку для загружаемого элемента
  const itemCount = hasNextPage ? items.length + 1 : items.length;

  // Проверяем загружен ли элемент
  const isItemLoaded = (index: number) => !hasNextPage || index < items.length;

  return (
    <InfiniteLoader
      isItemLoaded={isItemLoaded}
      itemCount={itemCount}
      loadMoreItems={isNextPageLoading ? () => {} : loadNextPage}
    >
      {({ onItemsRendered, ref }) => (
        <FixedSizeList
          height={600}
          itemCount={itemCount}
          itemSize={50}
          width="100%"
          onItemsRendered={onItemsRendered}
          ref={ref}
        >
          {({ index, style }) => {
            if (!isItemLoaded(index)) {
              return <div style={style}>Загрузка...</div>;
            }
            return (
              <div style={style}>
                {items[index].name}
              </div>
            );
          }}
        </FixedSizeList>
      )}
    </InfiniteLoader>
  );
}

Кастомный скроллбар и sticky header

import { FixedSizeList, ListChildComponentProps } from 'react-window';
import { forwardRef } from 'react';

// Кастомный внутренний контейнер для sticky заголовка
const innerElementType = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement>>(
  ({ style, ...rest }, ref) => (
    <div ref={ref} style={style} {...rest}>
      {/* Sticky заголовок */}
      <div style={{ position: 'sticky', top: 0, background: 'white', zIndex: 1 }}>
        <div style={{ display: 'flex', padding: '0 16px', height: 40 }}>
          <span style={{ flex: 1 }}>Имя</span>
          <span style={{ flex: 1 }}>Email</span>
          <span style={{ flex: 1 }}>Статус</span>
        </div>
      </div>
    </div>
  )
);

function TableWithHeader({ users }: { users: User[] }) {
  return (
    <FixedSizeList
      height={500}
      itemCount={users.length}
      itemSize={50}
      width="100%"
      innerElementType={innerElementType}
    >
      {({ index, style }: ListChildComponentProps) => (
        <div style={{ ...style, top: Number(style.top) + 40 }}>
          {/* Сдвигаем на высоту заголовка */}
          <span>{users[index].name}</span>
          <span>{users[index].email}</span>
          <span>{users[index].status}</span>
        </div>
      )}
    </FixedSizeList>
  );
}

Мемоизация компонента строки

Для максимальной производительности мемоизируйте компонент строки:

import { memo } from 'react';
import { areEqual, ListChildComponentProps } from 'react-window';

interface ItemData {
  items: Item[];
  selectedIds: Set<number>;
  onToggle: (id: number) => void;
}

// areEqual — функция сравнения пропсов из react-window
const MemoizedRow = memo(function Row({
  index,
  style,
  data,
}: ListChildComponentProps<ItemData>) {
  const { items, selectedIds, onToggle } = data;
  const item = items[index];
  const isSelected = selectedIds.has(item.id);

  return (
    <div
      style={style}
      className={`row ${isSelected ? 'row--selected' : ''}`}
      onClick={() => onToggle(item.id)}
    >
      <input type="checkbox" checked={isSelected} readOnly />
      <span>{item.name}</span>
    </div>
  );
}, areEqual);

function SelectableList({ items }: { items: Item[] }) {
  const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());

  // useCallback чтобы функция не создавалась заново
  const handleToggle = useCallback((id: number) => {
    setSelectedIds(prev => {
      const next = new Set(prev);
      next.has(id) ? next.delete(id) : next.add(id);
      return next;
    });
  }, []);

  // useMemo чтобы itemData не создавался заново при изменении selectedIds
  const itemData = useMemo(
    () => ({ items, selectedIds, onToggle: handleToggle }),
    [items, selectedIds, handleToggle]
  );

  return (
    <FixedSizeList
      height={500}
      itemCount={items.length}
      itemSize={50}
      width="100%"
      itemData={itemData}
    >
      {MemoizedRow}
    </FixedSizeList>
  );
}

Когда использовать react-window

Ситуация react-window нужен?
< 100 элементов ❌ Не нужен, overhead > выгоды
100 - 500 элементов 🟡 Зависит от сложности элементов
> 500 элементов ✅ Рекомендуется
Элементы с изображениями ✅ При любом количестве
Бесконечная прокрутка ✅ Обязательно
Таблицы с тысячами строк ✅ Обязательно

Краткое резюме

Компонент Когда использовать
FixedSizeList Все строки одинаковой высоты
VariableSizeList Строки разной высоты
FixedSizeGrid Двумерные данные (таблицы)
AutoSizer Адаптивный размер контейнера
InfiniteLoader Подгрузка данных при прокрутке

Дополнительные материалы

Стрелочка влевоRecoil — библиотека управления состоянием от FacebookСоздание таблиц в React гайд по react-tableСтрелочка вправо

Постройте личный план изучения React до уровня Middle — бесплатно!

React — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по React

Uncontrolled Components: когда DOM управляет даннымиRender Props: гибкое управление рендерингом в ReactПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаЛенивая загрузка: как ускорить React-приложение в разыHOC в React: мастерство композиции компонентовuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIКомпозиция компонентов в React: строим гибкие интерфейсыCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с данными
useState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияuseEffect в React что это и как использоватьuseContext — работа с контекстом в ReactОптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextКак предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуковuseState продвинутое использование в React
StrictMode в React — как находить ошибки на этапе разработкиСерверные компоненты React (RSC) — подробный разбор и практикаКак работает рендеринг в ReactЧто такое props в React и как их правильно использоватьКак работает JSX связка React и HTMLЧто такое React.js и как его использоватьКак использовать элементы в ReactКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьРабота с children в ReactПорталы в React: рендер компонентов вне иерархии DOMFragment в React: группировка элементов без лишних узлов DOM
Открыть базу знаний

Лучшие курсы по теме

изображение курса

React и Redux Toolkit

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий