Дайджесты новостей
Сравнительный студийный натюрморт двух карточек выпадающего списка: тяжеловесный DOM против виртуализированного скролла.

Дизайн-системы под нагрузкой: почему выпадающие списки замораживают интерфейс

Каждый фронтенд-разработчик хоть раз попадал в эту коварную ловушку. Дизайнеры приносят в Figma великолепный интерфейс фильтрации каталога: аккуратный выпадающий список (Select) с поиском, группировкой по категориям, красивыми анимациями раскрытия и мягкими тенями. Ты берешь готовую дизайн-систему — например, Material UI (MUI), Mantine или Ant Design — подключаешь компонент за пять минут, проверяешь на тестовом массиве из пяти элементов: всё летает, анимация плавная, заказчик доволен.

А затем приложение выходит в продакшен, с бэкенда прилетает реальный справочник номенклатуры на 2000 позиций, пользователь кликает по селекту — и вкладка браузера намертво замирает на две секунды. Курсор превращается в колесико ожидания, вентиляторы ноутбука начинают выть, а разгневанный клиент строчит жалобу в техподдержку.

Почему простые интерфейсные компоненты так катастрофически деградируют под нагрузкой? Ответ на этот вопрос дал французский инженер Ромен Клеман (Romain Clément), опубликовавший подробнейшее исследование производительности ведущих дизайн-систем, освещенное в выпуске React Status и в открытом бенчмарке компонентов.

Давай разберем результаты этих замеров и поймем, какие архитектурные компромиссы скрываются за красивыми фасадами библиотек.

Методология стресс-теста: измеряем реальные задержки

Клеман подошел к тестированию с предельной инженерной строгостью. Вместо синтетических абстрактных тестов он создал воспроизводимый тестовый стенд на базе автоматизированного фреймворка Playwright.

Условия эксперимента:

  1. Тестовый стенд рендерит типовой выпадающий список на 2500 элементов с текстовыми метками и иконками.
  2. Имитируется пользовательский клик для открытия выпадающего меню.
  3. Через Performance API и трассировку Chrome DevTools замеряются ключевые метрики:
    • TBT (Total Blocking Time): суммарное время блокировки основного потока браузера JavaScript-вычислениями;
    • Время первого рендеринга DOM (Mount Duration);
    • Потребление оперативной памяти кучей V8 (JS Heap Size).

Давай посмотрим, как выглядел базовый тестовый сценарий Playwright для фиксации задержек открытия меню:

import { test, expect } from '@playwright/test';

test('Замер времени блокировки потока при открытии селекта на 2500 элементов', async ({ page }) => {
  await page.goto('/benchmark/select');

  // Начинаем сбор метрик производительности через Chrome DevTools Protocol
  const client = await page.context().newCDPSession(page);
  await client.send('Performance.enable');

  const startMark = performance.now();
  
  // Кликаем по триггеру выпадающего списка
  await page.click('[data-testid="framework-select-trigger"]');
  
  // Ожидаем фактического появления выпадающего контейнера в DOM
  await page.waitForSelector('[data-testid="framework-select-dropdown"]');
  
  const renderTime = performance.now() - startMark;
  console.log(`Время раскрытия меню: ${renderTime.toFixed(2)} мс`);

  const metrics = await client.send('Performance.getMetrics');
  const jsHeapMetric = metrics.metrics.find(m => m.name === 'JSHeapUsedSize');
  console.log(`Память кучи JS: ${(jsHeapMetric!.value / 1024 / 1024).toFixed(2)} МБ`);

  expect(renderTime).toBeLessThan(500); // Ожидаемый порог отзывчивости
});

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

Цифры бенчмарка оказались шокирующими даже для опытных архитекторов интерфейсов:

  1. MUI (Material UI v6):
    • Время рендеринга меню: 1840 мс.
    • Потребление памяти кучи: 48 МБ.
    • Причина деградации: использование тяжелого CSS-in-JS движка (Emotion/Pigment). На каждый из 2500 пунктов списка MUI создает отдельное виртуальное дерево стилей, высчитывает классы в рантайме и регистрирует десятки слушателей событий.
  2. Ant Design (v5):
    • Время рендеринга меню: 620 мс.
    • Потребление памяти кучи: 28 МБ.
    • Причина: библиотека использует встроенную виртуализацию списков по умолчанию, однако накладные расходы на анимации и расчет доступности все равно вызывают кратковременный фриз основного потока.
  3. Mantine (v7):
    • Время рендеринга меню: 410 мс.
    • Потребление памяти: 19 МБ.
    • Отказ от CSS-in-JS в пользу нативных CSS-модулей в седьмой версии позволил Mantine существенно обойти конкурентов по скорости монтирования.
  4. Легковесные Headless-примитивы (Base UI / Radix с виртуализацией TanStack Virtual):
    • Время рендеринга меню: 42 мс.
    • Потребление памяти: 6 МБ.

Почувствуй колоссальную разницу: разрыв между классическим MUI и связкой headless-компонентов с виртуализацией составил более чем 40 раз по времени задержки интерфейса!

Архитектурный рецепт: как победить тормоза

Если в твоем проекте уже используется тяжелая дизайн-система, переписывать всё приложение с нуля обычно нереально. Но вылечить тормоза в критических местах можно с помощью двух проверенных шагов:

Во-первых, виртуализация. Никогда не позволяй браузеру рендерить в реальное DOM-дерево больше 20–30 видимых элементов списка одновременно:

import { useVirtualizer } from '@tanstack/react-virtual';
import React, { useRef } from 'react';

export function VirtualizedOptionsList({ items }: { items: string[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const rowVirtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 36, // Высота одного пункта списка в пикселях
    overscan: 5,            // Запас элементов сверху и снизу
  });

  return (
    <div ref={parentRef} className="h-64 overflow-auto border rounded">
      <div style={{ height: `${rowVirtualizer.getTotalSize()}px`, position: 'relative' }}>
        {rowVirtualizer.getVirtualItems().map((virtualRow) => (
          <div
            key={virtualRow.index}
            className="absolute top-0 left-0 w-full px-3 py-2 hover:bg-slate-100 cursor-pointer"
            style={{ transform: `translateY(${virtualRow.start}px)` }}
          >
            {items[virtualRow.index]}
          </div>
        ))}
      </div>
    </div>
  );
}

Во-вторых, отказ от рантайм CSS-in-JS на тяжелых списках в пользу Tailwind CSS или чистых CSS-модулей. Если вспомнить наш анализ того, как происходит архитектурная эрозия в веб-системах, слепое доверие популярным библиотекам без замера их поведения под нагрузкой — верный путь к потере пользователей.

Красивый дизайн обязан быть быстрым. Замеряйте производительность компонентов до релиза в продакшен, а не после жалоб пользователей!