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

Как работать с данными для отображения таблицы из 100000 полей?

1.8 Middle🔥 122 комментариев
#Оптимизация и производительность

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

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

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

Как работать с данными для отображения таблицы из 100000 полей

Это отличный вопрос, потому что 100000 строк в таблице — это реальная проблема, с которой сталкиваются в больших приложениях (финансовые системы, аналитические платформы, админ-панели).

Добавить в DOM 100000 элементов напрямую = зависло приложение. Давайте разберем решения.

Проблема: Rendering 100000 строк

// ❌ НИКОГДА так не делай
const rows = [];
for (let i = 0; i < 100000; i++) {
  rows.push(`<tr><td>${i}</td><td>Data ${i}</td></tr>`);
}
table.innerHTML = rows.join('');
// Браузер зависнет на 5-10 секунд!!

Почему это плохо:

  • Создание 100000 DOM элементов = огромный Reflow/Repaint
  • Использует много памяти
  • Блокирует main thread
  • Пользователь видит пустой экран

Решение 1: Virtualization (Самое эффективное)

Идея: Рендерить только видимые в окне просмотра строки, а не всё.

Если таблица высотой 600px и каждая строка 30px, видно максимум ~20 строк.

class VirtualizedTable {
  constructor(container, data, rowHeight = 30) {
    this.container = container;
    this.data = data;
    this.rowHeight = rowHeight;
    
    this.viewport = document.createElement('div');
    this.viewport.style.height = '600px';
    this.viewport.style.overflow = 'auto';
    this.container.appendChild(this.viewport);
    
    this.content = document.createElement('div');
    this.content.style.height = `${data.length * rowHeight}px`;
    this.viewport.appendChild(this.content);
    
    this.viewport.addEventListener('scroll', () => this.onScroll());
    this.onScroll();
  }
  
  onScroll() {
    const scrollTop = this.viewport.scrollTop;
    const viewportHeight = this.viewport.clientHeight;
    
    // Какие строки видны
    const startIndex = Math.floor(scrollTop / this.rowHeight);
    const endIndex = Math.ceil((scrollTop + viewportHeight) / this.rowHeight);
    
    // Рендерим только видимые + немного буфера (для smooth scrolling)
    const bufferSize = 10;
    const renderStart = Math.max(0, startIndex - bufferSize);
    const renderEnd = Math.min(this.data.length, endIndex + bufferSize);
    
    this.renderRows(renderStart, renderEnd);
  }
  
  renderRows(startIndex, endIndex) {
    let html = '';
    const offsetY = startIndex * this.rowHeight;
    
    for (let i = startIndex; i < endIndex; i++) {
      const row = this.data[i];
      html += `
        <div style="position: absolute; top: ${i * this.rowHeight}px; width: 100%; height: ${this.rowHeight}px;">
          <td>${row.id}</td>
          <td>${row.name}</td>
          <td>${row.value}</td>
        </div>
      `;
    }
    
    this.content.innerHTML = html;
  }
}

// Использование
const data = Array.from({ length: 100000 }, (_, i) => ({
  id: i,
  name: `Item ${i}`,
  value: Math.random()
}));

new VirtualizedTable(document.getElementById('app'), data);

Решение 2: React Virtual Scrolling

Если ты используешь React, есть отличные готовые библиотеки:

// react-window (рекомендуется)
import { FixedSizeList } from 'react-window';

function VirtualTable({ data }) {
  return (
    <FixedSizeList
      height={600}
      itemCount={data.length}
      itemSize={30}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          <td>{data[index].id}</td>
          <td>{data[index].name}</td>
          <td>{data[index].value}</td>
        </div>
      )}
    </FixedSizeList>
  );
}

// tanstack-table (раньше react-table)
import { useVirtualizer } from '@tanstack/react-virtual';
import { useTable } from '@tanstack/react-table';

function VirtualDataTable({ columns, data }) {
  const { getRowModel } = useTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });
  
  const rowVirtualizer = useVirtualizer({
    count: getRowModel().rows.length,
    size: 30, // row height
    overscan: 10, // буфер строк
  });
  
  return (
    <div style={{ height: '600px', overflow: 'auto' }}>
      {/* Рендерим только видимые строки */}
      {rowVirtualizer.getVirtualItems().map(virtualItem => (
        <div key={virtualItem.index} style={{ height: virtualItem.size }}>
          {/* Строка */}
        </div>
      ))}
    </div>
  );
}

Решение 3: Pagination (Для больших датасетов)

Загружай данные по частям, не всё сразу

class PaginatedTable {
  constructor(pageSize = 50) {
    this.pageSize = pageSize;
    this.currentPage = 1;
    this.totalRows = 100000;
  }
  
  async loadPage(pageNumber) {
    // Загружаем только нужную страницу с сервера
    const response = await fetch(
      `/api/data?page=${pageNumber}&limit=${this.pageSize}`
    );
    const data = await response.json();
    return data; // [page1Data, page2Data, ...]
  }
  
  async renderPage(pageNumber) {
    const data = await this.loadPage(pageNumber);
    const html = data.map(row => `<tr><td>${row.id}</td><td>${row.name}</td></tr>`).join('');
    document.getElementById('table-body').innerHTML = html;
    this.currentPage = pageNumber;
  }
}

// Использование
const table = new PaginatedTable(50);
table.renderPage(1); // Загруженные первые 50

// При клике "Next page"
document.getElementById('next-btn').addEventListener('click', () => {
  table.renderPage(table.currentPage + 1);
});

Решение 4: Infinite Scroll (Auto-loading)

Загружай при скролле (как в Instagram/Twitter)

class InfiniteScrollTable {
  constructor(container) {
    this.container = container;
    this.page = 1;
    this.pageSize = 50;
    this.isLoading = false;
    this.hasMore = true;
    
    this.setupIntersectionObserver();
  }
  
  setupIntersectionObserver() {
    // Создаем фейковый элемент в конце списка
    const sentinel = document.createElement('div');
    this.container.appendChild(sentinel);
    
    // Когда он становится видимым, загружаем новые данные
    const observer = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (entry.isIntersecting && this.hasMore && !this.isLoading) {
          this.loadMore();
        }
      });
    });
    
    observer.observe(sentinel);
  }
  
  async loadMore() {
    this.isLoading = true;
    
    const response = await fetch(
      `/api/data?page=${this.page}&limit=${this.pageSize}`
    );
    const { data, hasMore } = await response.json();
    
    const html = data.map(row => `<tr><td>${row.id}</td><td>${row.name}</td></tr>`).join('');
    this.container.innerHTML += html;
    
    this.hasMore = hasMore;
    this.page++;
    this.isLoading = false;
  }
}

// Использование
const scrollTable = new InfiniteScrollTable(document.getElementById('table'));
// Загружает первую партию, потом при скролле автоматически загружает больше

Решение 5: Server-side rendering + WebSocket для реального времени

Для ultra-large датасетов (миллионы строк)

// Backend (Node.js/Python)
app.get('/api/data/stream', (req, res) => {
  const { start, limit } = req.query;
  
  // Загруженіем только нужный диапазон из БД
  const data = database.query(
    'SELECT * FROM huge_table LIMIT ? OFFSET ?',
    [limit, start]
  );
  
  res.json({ data, total: database.count() });
});

// Frontend (React)
function StreamingTable() {
  const [data, setData] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  
  const loadData = async (start = 0) => {
    setIsLoading(true);
    const response = await fetch(`/api/data/stream?start=${start}&limit=100`);
    const { data } = await response.json();
    setData(prev => [...prev, ...data]);
    setIsLoading(false);
  };
  
  useEffect(() => {
    loadData(0);
  }, []);
  
  return (
    <div>
      <Table data={data} />
      {isLoading && <Spinner />}
      <button onClick={() => loadData(data.length)}>Load more</button>
    </div>
  );
}

Сравнение подходов

ПодходПлюсыМинусыКогда использовать
VirtualizationБыстро, все данные доступныСложнее реализоватьФильтрация/сортировка на клиенте
PaginationПростая, управляемая загрузкаНужно переходить на страницыТаблицы с сортировкой/фильтром
Infinite scrollUX похож на соц.сетиСложно с поиском концаНовостные ленты, галереи
StreamingОчень эффективно для больших данныхТребует backend оптимизацииФинансовые данные, логи, аналитика

Практический совет на интервью

Правильный ответ должен включать:

  1. Признай проблему: "Добавить 100000 элементов в DOM невозможно, это заморозит браузер"

  2. Предложи решение: "Использую Virtualization — рендерю только видимые строки"

  3. Упомяни альтернативы: "Или Pagination, или Infinite scroll, в зависимости от требований"

  4. Покажи код: "Библиотеки типа react-window делают это за тебя, но нужно понимать, как это работает"

  5. Обсуди trade-offs: "Virtualization быстро, но нельзя использовать Ctrl+F для поиска в DOM" "Pagination проще, но нужно знать, какую страницу выбрать"

Оптимизации дополнительно

// 1. Используй debounce для scroll event
const debounce = (fn, delay) => {
  let timeout;
  return (...args) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => fn(...args), delay);
  };
};

container.addEventListener('scroll', debounce(() => renderRows(), 50));

// 2. Используй requestAnimationFrame для smooth rendering
container.addEventListener('scroll', () => {
  requestAnimationFrame(() => renderRows());
});

// 3. Memoization в React
const Row = React.memo(({ item }) => (
  <div>{item.name}</div>
));

// 4. useMemo для вычислений
const memoizedData = useMemo(() => {
  return data.filter(d => d.active).sort();
}, [data]);

Вывод

Для 100000 строк не используй обычный подход:

  • ❌ Не добавляй всё в DOM
  • ❌ Не используй простые innerHTML
  • ❌ Не загружай всё сразу

Используй:

  • Virtualization (react-window, react-virtual) для фильтрации/поиска на клиенте
  • Pagination для упорядоченных данных (таблицы БД)
  • Infinite scroll для ленты новостей
  • Streaming с сервера для больших файлов

Это не просто оптимизация — это правильный подход для работы с большими данными. Главное — понимать trade-offs каждого метода.