← Назад к вопросам
Будет ли зависать Frontend при ста тысячах строк?
2.0 Middle🔥 81 комментариев
#Архитектура и паттерны
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Будет ли зависать Frontend при ста тысячах строк?
Этот вопрос касается производительности браузера при отображении большого количества элементов. Короткий ответ: Да, зависнет. Давайте разберёмся, почему и как это решать.
Почему 100,000 строк — это проблема?
1. DOM слишком большой
// ❌ Наивный подход — рендерим все элементы в DOM
const items = Array(100000).fill(null).map((_, i) => `Item ${i}`);
const html = items.map(item => `<div>${item}</div>`).join('');
document.body.innerHTML = html;
// Результат: браузер зависает на 5-30 секунд
2. Paint и Reflow
- Браузер должен "нарисовать" каждый элемент
- При изменении любого элемента пересчитывает макет для всех
- 100k элементов = 100k отрисовок
3. Память
- Каждый DOM элемент занимает ~200+ байт памяти
- 100k элементов = 20+ мегабайт только на DOM
- На мобильных это критично
4. JavaScript выполнение
- Парсинг и создание 100k объектов займёт время
- Event listeners на каждом элементе = ещё больше памяти
Решение 1: Виртуализация (Virtual Scrolling)
Идея: рендерим только видимые элементы!
// ✅ Virtual Scrolling
class VirtualScroller {
constructor(container, items, itemHeight) {
this.container = container;
this.items = items;
this.itemHeight = itemHeight;
this.visibleCount = Math.ceil(container.clientHeight / itemHeight);
this.scrollTop = 0;
this.container.addEventListener('scroll', () => this.onScroll());
this.render();
}
onScroll() {
this.scrollTop = this.container.scrollTop;
this.render();
}
render() {
// Вычисляем какие элементы видны
const startIndex = Math.floor(this.scrollTop / this.itemHeight);
const endIndex = startIndex + this.visibleCount + 1;
// Рендерим только видимые элементы
const html = this.items
.slice(startIndex, endIndex)
.map((item, i) => `
<div style="position: absolute; top: ${(startIndex + i) * this.itemHeight}px;">
${item}
</div>
`)
.join('');
this.container.innerHTML = html;
}
}
// Использование
const items = Array(100000).fill(null).map((_, i) => `Item ${i}`);
const scroller = new VirtualScroller(
document.getElementById('list'),
items,
50 // высота каждого элемента
);
// Результат: мгновенная загрузка, плавный скролл!
Производительность:
- Время загрузки: < 100ms (вместо 30 секунд)
- RAM использование: ~5MB (вместо 20MB+)
- Плавный скролл на 60 FPS
React: примеры с react-window
import { FixedSizeList as List } from 'react-window';
// ❌ Без виртуализации (зависает)
function BadList({ items }) {
return (
<div>
{items.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
// ✅ С виртуализацией (быстро)
function GoodList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<List
height={600}
itemCount={items.length}
itemSize={35}
width="100%"
>
{Row}
</List>
);
}
// Использование
const items = Array(100000).fill(null).map((_, i) => ({
id: i,
name: `Item ${i}`
}));
ReactDOM.render(
<GoodList items={items} />,
document.getElementById('root')
);
Решение 2: Пагинация
// ✅ Простая пагинация
class PaginatedList {
constructor(allItems, pageSize = 50) {
this.allItems = allItems;
this.pageSize = pageSize;
this.currentPage = 0;
}
getPage(pageNumber = this.currentPage) {
const start = pageNumber * this.pageSize;
const end = start + this.pageSize;
return this.allItems.slice(start, end);
}
nextPage() {
this.currentPage++;
return this.getPage();
}
get totalPages() {
return Math.ceil(this.allItems.length / this.pageSize);
}
}
// Использование
const list = new PaginatedList(Array(100000).fill(null), 50);
console.log(list.getPage()); // Первые 50 элементов
console.log(list.getPage(1)); // Элементы 50-100
Решение 3: Infinite Scroll (ленивая загрузка)
// ✅ Infinite scroll
class InfiniteScroll {
constructor(container, fetchFn) {
this.container = container;
this.fetchFn = fetchFn;
this.page = 0;
this.isLoading = false;
this.container.addEventListener('scroll', () => this.checkScroll());
this.load();
}
async load() {
if (this.isLoading) return;
this.isLoading = true;
const items = await this.fetchFn(this.page);
items.forEach(item => {
const div = document.createElement('div');
div.textContent = item;
this.container.appendChild(div);
});
this.page++;
this.isLoading = false;
}
checkScroll() {
const { scrollTop, clientHeight, scrollHeight } = this.container;
// Если пользователь прокручивает к концу
if (scrollTop + clientHeight > scrollHeight - 200) {
this.load();
}
}
}
// Использование
const scroller = new InfiniteScroll(
document.getElementById('list'),
async (page) => {
// API возвращает 50 элементов
const response = await fetch(`/api/items?page=${page}&limit=50`);
return response.json();
}
);
Решение 4: Server-Side Фильтрация и Поиск
// ✅ Вместо загрузки всех 100k — фильтруем на сервере
class FilteredList {
constructor(searchInput, container) {
this.searchInput = searchInput;
this.container = container;
this.searchInput.addEventListener('input', (e) => this.search(e.target.value));
}
async search(query) {
const response = await fetch(`/api/search?q=${query}`);
const results = await response.json();
this.container.innerHTML = results
.map(item => `<div>${item}</div>`)
.join('');
}
}
// Использование
const list = new FilteredList(
document.getElementById('searchInput'),
document.getElementById('results')
);
Бенчмарк: разные подходы
| Подход | Память | Загрузка | Скролл | Поиск |
|---|---|---|---|---|
| Все строки | 20MB+ | 30s | ❌ Зависает | Невозможен |
| Виртуализация | 2MB | 100ms | ✅ 60 FPS | OK |
| Пагинация | 1MB | 50ms | ✅ 60 FPS | OK |
| Infinite Scroll | 2MB | 50ms | ✅ 60 FPS | Нужен сервер |
| Server-Side | <1MB | 50ms | ✅ 60 FPS | ✅ Быстрый |
На практике: 100,000 строк таблицы
// React с ag-Grid (профессиональная таблица)
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-quartz.css';
function DataGrid() {
const rowData = Array(100000).fill(null).map((_, i) => ({
id: i,
name: `User ${i}`,
email: `user${i}@example.com`,
salary: Math.random() * 100000
}));
const columnDefs = [
{ field: 'id', width: 100 },
{ field: 'name', width: 150 },
{ field: 'email', width: 200 },
{ field: 'salary', width: 120 }
];
return (
<div className="ag-theme-quartz" style={{ height: '100vh', width: '100%' }}>
<AgGridReact
rowData={rowData}
columnDefs={columnDefs}
pagination={true}
paginationPageSize={50}
virtualizationPageSize={50}
/>
</div>
);
}
Чеклист для работы с большими списками
- Виртуальная прокрутка (react-window, react-virtualized)
- Пагинация на сервере
- Ленивая загрузка (infinite scroll)
- Фильтрация на бэкенде
- Кеширование результатов
- Дебаунс для поиска
- Оптимизированные key'и в React
- Мемоизация компонентов (React.memo)
- Lazy loading изображений
Вывод
100,000 строк — это НЕ проблема для современного веба, если:
- Используешь виртуализацию
- Имеешь хорошую пагинацию/infinite scroll
- Фильтруешь на сервере
- Оптимизируешь компоненты
Без оптимизации — да, браузер зависнет. С оптимизацией — будет летать!