Перейти к содержимому

9. Network Performance

Сетевые запросы — одно из главных узких мест. Каждый запрос = задержка DNS + TCP + TLS + TTFB.

HTTP/1.1: Один запрос за раз, очередь
HTTP/2: Мультиплексирование — много запросов в одном соединении
HTTP/3: UDP вместо TCP, лучше при потерях пакетов

Проверка: Chrome DevTools → Network → Protocol колонка

<!-- DNS Prefetch — заранее резолвим DNS -->
<link rel="dns-prefetch" href="//api.example.com">
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<link rel="dns-prefetch" href="//cdn.example.com">
<!-- Preconnect — TCP + TLS рукопожатие заранее -->
<link rel="preconnect" href="https://api.example.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Preload — загружаем критический ресурс заранее -->
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin>
<link rel="preload" href="/hero.webp" as="image" fetchpriority="high">
<link rel="preload" href="/critical.css" as="style">
<link rel="preload" href="/main.js" as="script">
<!-- Prefetch — загружаем следующую страницу -->
<link rel="prefetch" href="/about.html">
<link rel="prefetch" href="/assets/next-page-bundle.js">
<!-- Modulepreload — для ES-модулей -->
<link rel="modulepreload" href="/app.js">
# Nginx — Gzip
gzip on;
gzip_comp_level 6;
gzip_types text/plain text/css application/javascript application/json;
# Nginx — Brotli (быстрее и лучше сжимает)
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/javascript;
// Next.js — автоматически
// Compress включён по умолчанию
// Express
const compression = require('compression');
app.use(compression({ level: 6 }));

Результат: JS/CSS файлы сжимаются в 3-5 раз!

# Nginx — правильные Cache-Control заголовки
location /assets/ {
# Статика с хешем в имени — кэшируем навсегда
add_header Cache-Control "public, max-age=31536000, immutable";
}
location /api/ {
# API — без кэша или короткий
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
location = /index.html {
# HTML — проверяем актуальность
add_header Cache-Control "no-cache";
}
// Service Worker — продвинутое кэширование
const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = ['/', '/styles.css', '/app.js'];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS))
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(cached => {
return cached || fetch(event.request).then(response => {
caches.open(CACHE_NAME).then(cache => cache.put(event.request, response.clone()));
return response;
});
})
);
});
// ❌ REST — получаем всё
GET /api/users/123 → { id, name, email, avatar, bio, settings, posts, ... }
// ✅ GraphQL — только нужное
query {
user(id: "123") {
name
avatar
}
}
// ❌ 10 отдельных запросов
for (const id of userIds) {
await fetch(`/api/users/${id}`); // 10 round trips!
}
// ✅ Один батч-запрос
const users = await fetch('/api/users/batch', {
method: 'POST',
body: JSON.stringify({ ids: userIds }),
});
// ✅ DataLoader (для GraphQL/Node.js)
const loader = new DataLoader(async (ids) => {
const users = await db.user.findMany({ where: { id: { in: ids } } });
return ids.map(id => users.find(u => u.id === id));
});
// Кэшируем одинаковые запросы
const requestCache = new Map();
async function fetchWithCache(url) {
if (requestCache.has(url)) {
return requestCache.get(url); // возвращаем существующий Promise!
}
const promise = fetch(url).then(r => r.json());
requestCache.set(url, promise);
// Очищаем после успешной загрузки
promise.then(() => requestCache.delete(url));
return promise;
}
// При параллельных вызовах
fetchWithCache('/api/user'); // создаёт запрос
fetchWithCache('/api/user'); // возвращает тот же Promise — один запрос!
// Server-Sent Events для обновлений в реальном времени
const eventSource = new EventSource('/api/stream');
eventSource.addEventListener('update', (e) => {
updateUI(JSON.parse(e.data));
});
// WebSocket для двунаправленной связи
const ws = new WebSocket('wss://api.example.com/ws');
ws.addEventListener('message', ({ data }) => {
updateUI(JSON.parse(data));
});
// Next.js App Router — Response Streaming
export default async function Page() {
return (
<Suspense fallback={<Loading />}>
<SlowDataComponent /> {/* Стримится как готово */}
</Suspense>
);
}