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

Как передать на фронтенд большой объем данных?

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

Вывод

Для больших объёмов данных:

  1. По умолчанию — пагинация
  2. Для лучшего UX — infinite scroll
  3. Для real-time — WebSocket
  4. Для очень больших — streaming
  5. Всегда — сжатие и фильтрация
Как передать на фронтенд большой объем данных? | PrepBro