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
| Score | Rating | User Perception |
|---|---|---|
| ≤ 800ms | Good | Server responds quickly |
| 800ms - 1800ms | Needs Improvement | Noticeable server delay |
| > 1800ms | Poor | Server feels slow |
TTFB Breakdown
| Component | Typical Time | What You Control |
|---|---|---|
| DNS Lookup | 20-120ms | DNS provider |
| TCP Connection | 20-100ms | Server location |
| TLS Handshake | 30-150ms | TLS configuration |
| Server Processing | 50-500ms+ | Your code |
| Response Start | 10-50ms | Network |
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
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
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
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
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.
| Protocol | Key Benefits |
|---|---|
| HTTP/2 | Multiplexed connections, header compression, server push |
| HTTP/3 | 0-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
| Optimization | TTFB 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 |