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

30 июля 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 видимых элементов

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

Курс по теме

Освоить React и Redux Toolkit на курсе

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

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 — библиотека управления состоянием от FacebookReact Toastify - уведомления в ReactСтрелочка вправо

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

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

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

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

Все гайды по React

Uncontrolled Components: когда DOM управляет даннымиБезопасность в React: защита от XSS, CSRF и утечек данныхRender Props: гибкое управление рендерингом в ReactРефакторинг React-кода: техники и лучшие практикиReact Native приложение с нуля до публикации в сторахПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаИменование компонентов в React: соглашения и лучшие практикиЛенивая загрузка: как ускорить React-приложение в разыHOC в React: мастерство композиции компонентовuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIДокументирование компонентов в React: Storybook, JSDoc и READMEКомпозиция компонентов в React: строим гибкие интерфейсыКомментирование кода в React: когда и как писать комментарииCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с даннымиДоступность (a11y) в React: ARIA, семантика и клавиатурная навигация
Zustand — управление состоянием в ReactZod - валидация с TypeScriptYup - валидация схемXState - конечные автоматыТемизация в ReactТестирование хуковTailwind CSS с ReactStyled Components — стилизация через JSSWR - библиотека для запросовStorybook - документация компонентовSnapshots тестированиеRTK Query - работа с APIRedux Toolkit - современный ReduxRecoil — библиотека управления состоянием от FacebookВиртуализация списков с react-window: как отображать тысячи элементов без лаговReact Toastify - уведомления в ReactReact Testing LibraryСоздание таблиц в React гайд по react-tableReact Spring - анимацииРабота с формами и селектами в ReactReact Query (TanStack Query) - работа с серверомПлагины в React что это и как их использоватьReact PDF - работа с PDF файламиОбзор популярных библиотек для ReactReact Icons - библиотека иконок для ReactReact Hook Form — валидация форм в ReactReact Dropzone — загрузка файловПодключение Bootstrap к React-приложениюReact Beautiful DnD - перетаскивание элементовАнимация при монтировании компонентов в ReactМокирование APIMobX — реактивное управление состоянием в ReactМикрофронтенды с React (micro-frontends)Загрузка и индикаторыАнимация списков в ReactJotai - атомарное состояниеБесконечная прокруткаFramer Motion - библиотека анимацийEmotion — библиотека CSS-in-JSДинамические стили в ReactE2E тестирование с CypressCSSTransition - переходыCSS-in-JS — плюсы и минусыКонтекст vs Redux — когда что использоватьИспользование Chart.js в ReactAxios с ReactТестирование асинхронных компонентовОбработка ошибок API
useState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseOptimistic — оптимистичные обновления UIuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseFormStatus - отслеживание статуса отправки формыuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияХук use() в React 19useEffect в React что это и как использоватьuseContext — работа с контекстом в ReactuseCallback в React — мемоизация функций и оптимизация ре-рендеровuseActionState в React 19Оптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextНовые возможности React 19Как предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуковuseState продвинутое использование в React
Transition API — плавные обновления интерфейса в ReactReact Suspense — приостановка рендераStrictMode в React — как находить ошибки на этапе разработкиСерверные компоненты React (RSC) — подробный разбор и практикаКак работает рендеринг в ReactЧто такое props в React и как их правильно использоватьКак работает JSX связка React и HTMLЧто такое React.js и как его использоватьКак использовать элементы в ReactКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьРабота с children в ReactПорталы в React: рендер компонентов вне иерархии DOMFragment в React: группировка элементов без лишних узлов DOMCSS Modules в ReactConcurrent Mode — конкурентный режим в React
Открыть базу знаний

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

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

React и Redux Toolkit

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

TypeScript с нуля

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

Next.js - с нуля

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

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