← Назад к вопросам
Как передать на фронтенд большой объем данных?
2.2 Middle🔥 191 комментариев
#REST API и HTTP#Архитектура и паттерны
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Передача больших объёмов данных на фронтенд
Передача больших объёмов данных требует тщательного подхода — нужна оптимизация на каждом уровне.
1. Пагинация (основной метод)
Backend (FastAPI):
from fastapi import FastAPI, Query
from pydantic import BaseModel
from typing import List
app = FastAPI()
class ItemResponse(BaseModel):
id: int
name: str
price: float
class PaginatedResponse(BaseModel):
items: List[ItemResponse]
total: int
page: int
page_size: int
total_pages: int
@app.get("/items", response_model=PaginatedResponse)
async def get_items(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100)
):
skip = (page - 1) * page_size
# Получаем всего
total = db.query(Item).count()
# Получаем только нужную страницу
items = db.query(Item).offset(skip).limit(page_size).all()
return PaginatedResponse(
items=items,
total=total,
page=page,
page_size=page_size,
total_pages=(total + page_size - 1) // page_size
)
Frontend (React):
import { useState, useEffect } from 'react';
export function ItemsList() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const pageSize = 20;
useEffect(() => {
fetch(`/api/items?page=${page}&page_size=${pageSize}`)
.then(res => res.json())
.then(data => {
setItems(data.items);
setTotal(data.total);
});
}, [page]);
return (
<div>
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<div>
<button onClick={() => setPage(p => p - 1)} disabled={page === 1}>
Назад
</button>
<span>Страница {page} из {Math.ceil(total / pageSize)}</span>
<button onClick={() => setPage(p => p + 1)}>
Далее
</button>
</div>
</div>
);
}
2. Infinite Scroll (бесконечная прокрутка)
Backend:
@app.get("/items/infinite")
async def get_items_infinite(
cursor: int = Query(0),
limit: int = Query(20, le=100)
):
items = db.query(Item).offset(cursor).limit(limit).all()
return {
"items": items,
"next_cursor": cursor + limit if len(items) == limit else None
}
Frontend с React Intersection Observer:
import { useEffect, useRef, useCallback } from 'react';
export function InfiniteList() {
const [items, setItems] = useState([]);
const [cursor, setCursor] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const endRef = useRef(null);
const loadMore = useCallback(async () => {
if (isLoading) return;
setIsLoading(true);
const res = await fetch(`/api/items/infinite?cursor=${cursor}&limit=20`);
const data = await res.json();
setItems(prev => [...prev, ...data.items]);
if (data.next_cursor) setCursor(data.next_cursor);
setIsLoading(false);
}, [cursor, isLoading]);
useEffect(() => {
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting) {
loadMore();
}
},
{ threshold: 0.1 }
);
if (endRef.current) {
observer.observe(endRef.current);
}
return () => observer.disconnect();
}, [loadMore]);
return (
<div>
{items.map(item => (
<div key={item.id}>{item.name}</div>
))}
<div ref={endRef} style={{ height: '20px' }}>
{isLoading && <p>Загрузка...</p>}
</div>
</div>
);
}
3. Streaming (потоковая передача)
Backend:
from fastapi.responses import StreamingResponse
import json
@app.get("/items/stream")
async def stream_items():
async def generate():
# Потоком отправляем данные
yield b"["
first = True
for item in db.query(Item).all():
if not first:
yield b",\n"
yield json.dumps(item.dict()).encode() + b""
first = False
yield b"]"
return StreamingResponse(generate(), media_type="application/json")
Frontend:
export async function streamItems() {
const response = await fetch('/api/items/stream');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value);
// Обрабатываем полные объекты
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
const json = line.trim().replace(/,$/, '');
if (json) {
console.log(JSON.parse(json));
}
}
}
}
4. Server-Sent Events (SSE)
Backend:
from fastapi.responses import StreamingResponse
@app.get("/items/sse")
async def stream_items_sse():
async def generate():
for item in db.query(Item).all():
yield f"data: {json.dumps(item.dict())}\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")
Frontend:
export function useSSE(url: string) {
const [items, setItems] = useState([]);
useEffect(() => {
const eventSource = new EventSource(url);
eventSource.onmessage = (event) => {
const item = JSON.parse(event.data);
setItems(prev => [...prev, item]);
};
eventSource.onerror = () => {
eventSource.close();
};
return () => eventSource.close();
}, [url]);
return items;
}
5. WebSocket для real-time данных
Backend:
from fastapi import WebSocket, WebSocketDisconnect
@app.websocket("/ws/items")
async def websocket_items(websocket: WebSocket):
await websocket.accept()
try:
while True:
# Отправляем новые данные по мере их появления
for item in db.query(Item).all():
await websocket.send_json(item.dict())
await asyncio.sleep(5) # Обновляем каждые 5 сек
except WebSocketDisconnect:
pass
Frontend:
export function useWebSocket(url: string) {
const [items, setItems] = useState([]);
useEffect(() => {
const ws = new WebSocket(url);
ws.onmessage = (event) => {
const item = JSON.parse(event.data);
setItems(prev => [...prev, item]);
};
return () => ws.close();
}, [url]);
return items;
}
6. Сжатие данных
Backend (gzip автоматически):
from fastapi.middleware.gzip import GZIPMiddleware
app.add_middleware(GZIPMiddleware, minimum_size=1000)
Ручное сжатие для очень больших данных:
import gzip
import json
@app.get("/items/compressed")
async def get_items_compressed():
items = db.query(Item).all()
json_data = json.dumps([item.dict() for item in items])
compressed = gzip.compress(json_data.encode())
return Response(
content=compressed,
media_type="application/json",
headers={"Content-Encoding": "gzip"}
)
7. Фильтрация на backend
Отправляй только нужные поля:
class ItemMinimal(BaseModel):
id: int
name: str
# Не отправляем текст, описание, и т.д.
@app.get("/items", response_model=List[ItemMinimal])
async def get_items():
return db.query(Item).all()
8. Кеширование на frontend
const cache = new Map();
export async function getCachedItems(page: number) {
const cacheKey = `items-${page}`;
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const response = await fetch(`/api/items?page=${page}`);
const data = await response.json();
cache.set(cacheKey, data);
return data;
}
Выбор метода по сценарию
Маленьких объём (< 10K записей):
- Пагинация
- Простая JSON ответ
Средний объём (10K - 100K):
- Infinite scroll
- Пагинация с индексами
- Сжатие gzip
Огромный объём (> 100K):
- Streaming
- WebSocket
- Server-Sent Events
- Фильтрация на backend
Real-time обновления:
- WebSocket
- Server-Sent Events
Чеклист оптимизации
- Используешь пагинацию по умолчанию
- Отправляешь только нужные поля
- Включен gzip compression
- Кешируешь на frontend
- Lazy loading изображений
- Используешь индексы в БД
- Мониторишь размер ответов
- Ограничиваешь максимальный page_size
Вывод
Для больших объёмов данных:
- По умолчанию — пагинация
- Для лучшего UX — infinite scroll
- Для real-time — WebSocket
- Для очень больших — streaming
- Всегда — сжатие и фильтрация