Skip to content
21 / 36

Π§Ρ‚ΠΎ Ρ‚Π°ΠΊΠΎΠ΅ Time to First Byte (TTFB) ΠΈ ΠΊΠ°ΠΊ Π΅Π³ΠΎ ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ?


ΠŸΠΎΠ»Π½Ρ‹ΠΉ ΠΎΡ‚Π²Π΅Ρ‚

TTFB складываСтся ΠΈΠ· Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ… этапов:

JavaScript
Π‘Ρ€Π°ΡƒΠ·Π΅Ρ€                                    Π‘Π΅Ρ€Π²Π΅Ρ€
|  |
| --- DNS Lookup ---------------------------> |
| --- TCP Handshake ------------------------> |
| --- TLS Handshake (Ссли HTTPS) ----------> |
| --- ΠžΡ‚ΠΏΡ€Π°Π²ΠΊΠ° HTTP-запроса ----------------> |
| [ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° Π½Π° сСрвСрС] |
| <-- ΠŸΠ΅Ρ€Π²Ρ‹ΠΉ Π±Π°ΠΉΡ‚ ΠΎΡ‚Π²Π΅Ρ‚Π° ------------------- |
|  |
| <====== Всё это TTFB ===============> |

Π˜Π·ΠΌΠ΅Ρ€Π΅Π½ΠΈΠ΅ TTFB Ρ‡Π΅Ρ€Π΅Π· Performance API:

JavaScript
// Π§Π΅Ρ€Π΅Π· Navigation Timing API
const [entry] = performance.getEntriesByType('navigation');

const ttfb = entry.responseStart - entry.requestStart;
console.log(`TTFB: ${ttfb.toFixed(2)} ms`);

// Π‘ΠΎΠ»Π΅Π΅ Π΄Π΅Ρ‚Π°Π»ΡŒΠ½Π°Ρ Ρ€Π°Π·Π±ΠΈΠ²ΠΊΠ°
const dns = entry.domainLookupEnd - entry.domainLookupStart;
const tcp = entry.connectEnd - entry.connectStart;
const tls = entry.secureConnectionStart > 0
? entry.connectEnd - entry.secureConnectionStart
: 0;
const serverTime = entry.responseStart - entry.requestStart;

console.log(`DNS: ${dns.toFixed(2)} ms`);
console.log(`TCP: ${tcp.toFixed(2)} ms`);
console.log(`TLS: ${tls.toFixed(2)} ms`);
console.log(`Server processing: ${serverTime.toFixed(2)} ms`);

Бпособы ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·Π°Ρ†ΠΈΠΈ:

JavaScript
// 1. Π‘Π΅Ρ€Π²Π΅Ρ€Π½Ρ‹ΠΉ кСш -- самый большой Π²Ρ‹ΠΈΠ³Ρ€Ρ‹Ρˆ

// Express + node-cache
import NodeCache from 'node-cache';

const cache = new NodeCache({ stdTTL: 300 }); // 5 ΠΌΠΈΠ½ΡƒΡ‚

app.get('/api/products', (req, res) => {
    const cached = cache.get('products');
    if (cached) {
      return res.json(cached); // ΠΌΠ³Π½ΠΎΠ²Π΅Π½Π½Ρ‹ΠΉ ΠΎΡ‚Π²Π΅Ρ‚ ΠΈΠ· кСша
    }

    const products = db.query('SELECT * FROM products');
    cache.set('products', products);
    res.json(products);
  });

JavaScript
// 2. ΠžΠΏΡ‚ΠΈΠΌΠΈΠ·Π°Ρ†ΠΈΡ Π‘Π”-запросов
// ❌ N+1 запросов -- ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ ΠΏΠΎΡ€ΠΎΠΆΠ΄Π°Π΅Ρ‚ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹ΠΉ запрос
app.get('/api/posts', async (req, res) => {
    const posts = await db.query('SELECT * FROM posts LIMIT 20');
    for (const post of posts) {
      post.author = await db.query(
        'SELECT * FROM users WHERE id = ?', [post.authorId]
      );
    }
    res.json(posts);
  });

// βœ… Один запрос с JOIN
app.get('/api/posts', async (req, res) => {
    const posts = await db.query(`
      SELECT p.*, u.name as authorName, u.avatar as authorAvatar
      FROM posts p
      JOIN users u ON p.authorId = u.id
      LIMIT 20
      `);
    res.json(posts);
  });

HTML
<!-- 3. РСсурсныС подсказки Π² HTML -->
<!-- DNS prefetch для сторонних Π΄ΠΎΠΌΠ΅Π½ΠΎΠ² -->
<link rel="dns-prefetch" href="https://api.example.com">
<!-- Preconnect -- DNS + TCP + TLS Π·Π°Ρ€Π°Π½Π΅Π΅ -->
<link rel="preconnect" href="https://api.example.com" crossorigin>
<!-- 103 Early Hints -- сСрвСр отправляСт подсказки Π΄ΠΎ основного ΠΎΡ‚Π²Π΅Ρ‚Π° -->

Nginx

# 4. Настройка CDN / nginx для ΠΊΠ΅ΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡ

# nginx.conf
proxy_cache_path /var/cache/nginx levels=1:2
  keys_zone=app_cache:10m max_size=1g inactive=60m;

server {
    location /api/ {
        proxy_cache app_cache;
        proxy_cache_valid 200 10m;
        proxy_cache_use_stale error timeout updating;
        proxy_pass http://backend;
    }
}

πŸ“Œ Π Π΅Π°Π»ΡŒΠ½Ρ‹Π΅ кСйсы

Диагностика ΠΌΠ΅Π΄Π»Π΅Π½Π½ΠΎΠ³ΠΎ TTFB:

JavaScript
// Π‘ΠΊΡ€ΠΈΠΏΡ‚ для ΠΌΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³Π° TTFB Π² production
const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      if (entry.entryType === 'navigation') {
        const ttfb = entry.responseStart - entry.requestStart;

        // ΠžΡ‚ΠΏΡ€Π°Π²Π»ΡΠ΅ΠΌ ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΡƒ Π² Π°Π½Π°Π»ΠΈΡ‚ΠΈΠΊΡƒ
        if (ttfb > 800) {
          sendToAnalytics('slow_ttfb', {
              ttfb,
              url: entry.name,
              transferSize: entry.transferSize,
              serverTiming: entry.serverTiming,
            });
        }
      }
    }
  });

observer.observe({ type: 'navigation', buffered: true });

Server-Timing Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ для ΠΎΡ‚Π»Π°Π΄ΠΊΠΈ сСрвСрной части:

JavaScript
// Π‘Π΅Ρ€Π²Π΅Ρ€ ΠΏΠΎΠΊΠ°Π·Ρ‹Π²Π°Π΅Ρ‚, Π½Π° Ρ‡Ρ‚ΠΎ ΡƒΡˆΠ»ΠΎ врСмя
app.get('/api/data', async (req, res) => {
    const start = performance.now();

    const dbStart = performance.now();
    const data = await db.query('SELECT ...');
    const dbTime = performance.now() - dbStart;

    const cacheStart = performance.now();
    await redis.set('key', JSON.stringify(data));
    const cacheTime = performance.now() - cacheStart;

    res.set('Server-Timing', [
        `db;dur=${dbTime.toFixed(1)};desc="Database"`,
        `cache;dur=${cacheTime.toFixed(1)};desc="Cache write"`,
        `total;dur=${(performance.now() - start).toFixed(1)}`,
      ].join(', '));

    res.json(data);
  });

// Π’ΠΈΠ΄Π½ΠΎ Π² DevTools Π½Π° Π²ΠΊΠ»Π°Π΄ΠΊΠ΅ Network (Timing)

РСзюмС

TTFB -- пСрвая ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠ° Π² Ρ†Π΅ΠΏΠΎΡ‡ΠΊΠ΅ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ страницы. Если TTFB ΠΌΠ΅Π΄Π»Π΅Π½Π½Ρ‹ΠΉ, всС ΠΎΡΡ‚Π°Π»ΡŒΠ½Ρ‹Π΅ ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ (FCP, LCP) Π±ΡƒΠ΄ΡƒΡ‚ ΠΏΠ»ΠΎΡ…ΠΈΠΌΠΈ автоматичСски. ΠžΡΠ½ΠΎΠ²Π½Ρ‹Π΅ Ρ‚ΠΎΡ‡ΠΊΠΈ ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·Π°Ρ†ΠΈΠΈ: сСрвСрный кСш, CDN, оптимизация запросов ΠΊ Π‘Π”, preconnect/dns-prefetch, HTTP/2 ΠΈ 103 Early Hints.

Π§Ρ‚ΠΎ Ρ‚Π°ΠΊΠΎΠ΅ Time to First Byte (TTFB) ΠΈ ΠΊΠ°ΠΊ Π΅Π³ΠΎ ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ? | JScriptiser