perf hacks
Core Web VitalsTTFB

TTFB - Time to First Byte

Optimize server response time for faster page loads

Time to First Byte (TTFB) measures the time from when the browser requests a page to when it receives the first byte of the response. It's a foundational metric that affects all other performance metrics.

Understanding TTFB

Score Thresholds

ScoreRatingUser Perception
≤ 800msGoodServer responds quickly
800ms - 1800msNeeds ImprovementNoticeable server delay
> 1800msPoorServer feels slow

TTFB Breakdown

TTFB=DNS+TCP+TLS+Request+Server Processing+Response Start\text{TTFB} = \text{DNS} + \text{TCP} + \text{TLS} + \text{Request} + \text{Server Processing} + \text{Response Start}
ComponentTypical TimeWhat You Control
DNS Lookup20-120msDNS provider
TCP Connection20-100msServer location
TLS Handshake30-150msTLS configuration
Server Processing50-500ms+Your code
Response Start10-50msNetwork

Why TTFB Matters

TTFB directly impacts:

  • FCP: First content can't paint until bytes arrive
  • LCP: Delayed start = delayed largest paint
  • User Perception: Slow TTFB = spinning browser tab

A 100ms improvement in TTFB typically results in 50-100ms improvement in LCP.

Next.js TTFB Optimization

Use Static Generation

// ❌ Bad: Server processes every request
export async function getServerSideProps() {
  const data = await db.query('SELECT * FROM posts');
  return { props: { data } };
}
// TTFB: 200-500ms (depends on DB)

// ✅ Good: Pre-rendered at build time
export async function getStaticProps() {
  const data = await db.query('SELECT * FROM posts');
  return { props: { data } };
}
// TTFB: 20-50ms (just CDN latency)

Implement ISR for Dynamic Content

// Content that changes, but not every request
export async function getStaticProps() {
  const products = await fetchProducts();

  return {
    props: { products },
    revalidate: 60,
  };
}

// TTFB: ~50ms (cached) or ~300ms (revalidating)

Use Edge Runtime

// Move computation closer to users
export const runtime = 'edge';

export async function GET(request: Request) {
  const data = await fetchFromEdgeCache();
  return Response.json(data);
}

// TTFB: 20-100ms (depending on edge location)

Optimize Database Queries

// ❌ Bad: N+1 query problem
export async function getServerSideProps() {
  const posts = await db.query('SELECT * FROM posts');

  for (const post of posts) {
    post.author = await db.query(
      'SELECT * FROM users WHERE id = ?',
      [post.authorId]
    );
  }

  return { props: { posts } };
}
// TTFB: 500ms+ (multiple round trips)

// ✅ Good: Single query with JOIN
export async function getServerSideProps() {
  const posts = await db.query(`
    SELECT posts.*, users.name as authorName
    FROM posts
    JOIN users ON posts.authorId = users.id
  `);

  return { props: { posts } };
}
// TTFB: 100-200ms (single round trip)

Add Response Caching

app/api/data/route.ts
export async function GET() {
  const data = await fetchExpensiveData();

  return Response.json(data, {
    headers: {
      'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
    },
  });
}

// middleware.ts - Cache at edge
export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // Cache static assets aggressively
  if (request.nextUrl.pathname.startsWith('/static')) {
    response.headers.set(
      'Cache-Control',
      'public, max-age=31536000, immutable'
    );
  }

  return response;
}

CDN & Edge Strategies

Configure CDN Caching

next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/api/products',
        headers: [
          {
            key: 'Cache-Control',
            value: 's-maxage=60, stale-while-revalidate=300',
          },
        ],
      },
      {
        source: '/:path*',
        headers: [
          {
            key: 'X-DNS-Prefetch-Control',
            value: 'on',
          },
        ],
      },
    ];
  },
};

Use Edge Middleware Wisely

middleware.ts
export function middleware(request: NextRequest) {
  // ❌ Bad: Heavy computation in middleware
  const result = heavyComputation();

  // ✅ Good: Fast, simple checks
  const isAuthenticated = request.cookies.has('session');

  if (!isAuthenticated && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

// Limit middleware scope
export const config = {
  matcher: ['/dashboard/:path*', '/api/protected/:path*'],
};

Streaming for Perceived Performance

// Even with slow TTFB, streaming improves perceived performance
import { Suspense } from 'react';

export default function Page() {
  return (
    <>
      {/* Shell streams immediately */}
      <Header />

      {/* Slow data streams when ready */}
      <Suspense fallback={<ProductsSkeleton />}>
        <SlowProducts />
      </Suspense>
    </>
  );
}

async function SlowProducts() {
  const products = await fetchProducts(); // 500ms
  return <ProductGrid products={products} />;
}

Connection Optimization

Preconnect to Critical Origins

app/layout.tsx
export default function Layout({ children }) {
  return (
    <html>
      <head>
        {/* Preconnect to API server */}
        <link rel="preconnect" href="https://api.example.com" />

        {/* Preconnect to CDN */}
        <link rel="preconnect" href="https://cdn.example.com" />

        {/* DNS prefetch for less critical origins */}
        <link rel="dns-prefetch" href="https://analytics.example.com" />
      </head>
      <body>{children}</body>
    </html>
  );
}

HTTP/2 and HTTP/3

Vercel automatically uses HTTP/2+. For self-hosted deployments, ensure your server supports modern protocols.

ProtocolKey Benefits
HTTP/2Multiplexed connections, header compression, server push
HTTP/30-RTT connections, improved packet loss handling, QUIC transport

Measuring TTFB

import { onTTFB, type TTFBMetric } from 'web-vitals';

export function measureTTFB() {
  onTTFB((metric: TTFBMetric) => {
    console.log('TTFB:', metric.value, 'ms');

    // Breakdown available in navigation timing
    const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;

    console.log({
      dns: nav.domainLookupEnd - nav.domainLookupStart,
      tcp: nav.connectEnd - nav.connectStart,
      tls: nav.secureConnectionStart > 0
        ? nav.connectEnd - nav.secureConnectionStart
        : 0,
      request: nav.responseStart - nav.requestStart,
    });

    sendToAnalytics({
      name: 'TTFB',
      value: metric.value,
      rating: metric.rating,
    });
  });
}

TTFB Optimization Checklist

Rendering Strategy

Use static generation for static content Pre-rendered pages have near-instant TTFB from CDN.

Implement ISR for semi-dynamic content Get static speed with periodic background updates.

Use streaming for slow data fetches Send shell immediately while data loads.

Consider Edge Runtime for global users Run code closer to users for lower latency.

Caching

Set appropriate Cache-Control headers Configure browser and CDN caching properly.

Use CDN edge caching Cache responses at edge locations worldwide.

Implement stale-while-revalidate Serve cached content while fetching fresh data.

Cache database queries Use unstable_cache or external cache layer.

Server Optimization

Optimize database queries Slow queries are the biggest TTFB killer.

Use connection pooling Reuse database connections across requests.

Implement query result caching Cache expensive query results in memory or KV store.

Reduce server-side computation Move heavy processing to build time or background jobs.

Network

Use preconnect for critical origins Establish connections to APIs early.

Enable HTTP/2 or HTTP/3 Multiplexed connections reduce latency.

Minimize redirects Each redirect adds a full round trip.

Use a fast DNS provider DNS lookup is the first delay in every request.

Real-World Impact

OptimizationTTFB Improvement
Static generation-200ms to -1000ms
ISR caching-100ms to -500ms
Edge runtime-50ms to -300ms
Query optimization-100ms to -500ms
CDN edge caching-50ms to -200ms

On this page