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

Приведи пример сложного решения за последние полгода-год

1.0 Junior🔥 81 комментариев
#JavaScript Core

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

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

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

Пример сложного решения: Оптимизация рендеринга крупной таблицы с виртуализацией и Web Workers

За последний год наиболее сложной задачей стала оптимизация производительности для финансового веб-приложения, работающего с огромными массивами данных. Мы столкнулись с проблемой рендеринга таблицы, содержащей до 100 000 строк и 50 столбцов. Нагрузка на DOM и блокировка основного потока делали интерфейс полностью неотзывчивым. Решение потребовало комплексного подхода, сочетающего виртуализацию, Web Workers и ленивую загрузку.

Проблемы и анализ

Исходный компонент таблицы рендерил все строки одновременно, что приводило к:

  • Задержкам ввода (>5 секунд при скролле).
  • Потреблению памяти >500 МБ в браузере.
  • Блокировке основного потока из-за тяжелых вычислений (фильтрация, сортировка).

Реализованное решение

1. Виртуализация с использованием библиотеки TanStack Table

Мы выбрали TanStack Table (ранее React Table) за его гибкость и низкий уровень абстракции. Реализовали окно виртуализации, отображающее только видимые строки (~30-50 вместо 100 000).

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

const VirtualizedTable = ({ data, columns }) => {
  const tableContainerRef = useRef(null);
  
  const virtualizer = useVirtualizer({
    count: data.length,
    getScrollElement: () => tableContainerRef.current,
    estimateSize: () => 40, // Высота строки
    overscan: 10, // Буферные строки для плавного скролла
  });

  return (
    <div ref={tableContainerRef} style={{ height: '600px', overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
        {virtualizer.getVirtualItems().map((virtualRow) => (
          <TableRow
            key={virtualRow.key}
            data={data[virtualRow.index]}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualRow.size}px`,
              transform: `translateY(${virtualRow.start}px)`,
            }}
          />
        ))}
      </div>
    </div>
  );
};

2. Вынос вычислений в Web Worker

Фильтрация и сортировка выполнялись в отдельном потоке через Web Worker, чтобы не блокировать UI.

// worker.js
self.onmessage = (event) => {
  const { data, filters, sortBy } = event.data;
  
  // Тяжелая логика фильтрации/сортировки
  const filtered = data.filter(item => 
    filters.every(f => item[f.field].includes(f.value))
  );
  const sorted = filtered.sort((a, b) => 
    a[sortBy.field] > b[sortBy.field] ? 1 : -1
  );
  
  self.postMessage(sorted);
};

// main.js
const worker = new Worker('./worker.js');
worker.postMessage({ data: hugeDataset, filters, sortBy });
worker.onmessage = (event) => {
  setVisibleData(event.data.slice(0, 100)); // Ленивая загрузка
};

3. Ленивая загрузка данных

Мы разделили данные на чанки по 1000 записей и загружали их по мере скролла, интегрируя с Intersection Observer.

const LazyLoader = () => {
  const [loadedChunks, setLoadedChunks] = useState(0);
  
  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting && loadedChunks * 1000 < totalData) {
        fetchChunk(loadedChunks).then(newData => {
          setData(prev => [...prev, ...newData]);
          setLoadedChunks(prev => prev + 1);
        });
      }
    });
    
    observer.observe(loadTriggerRef.current);
    return () => observer.disconnect();
  }, [loadedChunks]);
  
  return <div ref={loadTriggerRef} />;
};

4. Оптимизация перерисовок с React.memo и useMemo

Каждая строка таблицы была обернута в React.memo, а вычисляемые значения кэшированы через useMemo.

const TableRow = React.memo(({ data }) => {
  const formattedValues = useMemo(() => 
    formatData(data), [data]
  );
  
  return <tr>{formattedValues}</tr>;
});

Результаты

После внедрения решения:

  • Время первого рендера сократилось с 12 до 0.3 секунд.
  • Потребление памяти упало на 80% (до ~100 МБ).
  • Плавность скролла достигла 60 FPS даже на слабых устройствах.
  • Основной поток больше не блокировался вычислениями.

Выводы

Этот опыт показал, что работа с большими данными на фронтенде требует глубокого понимания:

  • Механизмов рендеринга браузера и работы Event Loop.
  • Архитектурных паттернов для разделения потоков.
  • Инструментов виртуализации и их интеграции с фреймворками.

Решение потребовало не только технических навыков, но и тщательного профилирования производительности (Chrome DevTools, React Profiler) и а/б тестирования для проверки влияния на UX. В итоге мы создали масштабируемую систему, которую адаптировали и для других компонентов приложения.

Приведи пример сложного решения за последние полгода-год | PrepBro