Как работать с данными для отображения таблицы из 100000 полей?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Как работать с данными для отображения таблицы из 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 scroll | UX похож на соц.сети | Сложно с поиском конца | Новостные ленты, галереи |
| Streaming | Очень эффективно для больших данных | Требует backend оптимизации | Финансовые данные, логи, аналитика |
Практический совет на интервью
Правильный ответ должен включать:
-
Признай проблему: "Добавить 100000 элементов в DOM невозможно, это заморозит браузер"
-
Предложи решение: "Использую Virtualization — рендерю только видимые строки"
-
Упомяни альтернативы: "Или Pagination, или Infinite scroll, в зависимости от требований"
-
Покажи код: "Библиотеки типа react-window делают это за тебя, но нужно понимать, как это работает"
-
Обсуди 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 каждого метода.