Fix TTFB on Vercel: Optimize Time to First Byte
Vercel is built for performance, but misconfigured projects can still suffer from slow TTFB. Common causes include cold starts on serverless functions, missing cache headers, over-reliance on SSR when ISR or static generation would be faster, and Edge Runtime not being enabled for latency-sensitive routes.
Vercel deploys to a global edge network with 30+ points of presence. Static assets and ISR pages are served from these edge locations with single-digit millisecond TTFB. However, serverless functions and SSR routes still execute in a single region (defaulting to Washington, D.C.) unless you explicitly configure Edge Runtime or specify function regions.
This guide covers five Vercel-specific optimizations: leveraging ISR and static generation, configuring Edge Runtime, optimizing function regions, managing cache headers, and eliminating cold starts. These techniques bring typical Vercel TTFB from 500-1500ms down to 30-80ms for most pages.
Expected results
Following all steps in this guide typically produces these improvements:
Before
1.2s
TTFB (Poor) -- SSR functions in single region with cold starts and no caching strategy
After
45ms
TTFB (Good) -- ISR with edge caching and optimized function regions for dynamic routes
Step-by-step fix
Use static generation and ISR for content pages
The fastest TTFB on Vercel comes from static pages served directly from the edge CDN. For pages that change infrequently (blog posts, docs, product pages), use Static Site Generation (SSG) or Incremental Static Regeneration (ISR). ISR lets you serve cached static HTML while revalidating in the background, combining the speed of static with the freshness of dynamic.
// app/blog/[slug]/page.tsx
// ISR: cached at edge, revalidates every 60 seconds
export const revalidate = 60;
// Pre-generate top pages at build time (instant TTFB)
export async function generateStaticParams() {
const posts = await getPopularPosts(200);
return posts.map(p => ({ slug: p.slug }));
}
export default async function BlogPost({ params }) {
const post = await getPost(params.slug);
if (!post) notFound();
return <Article post={post} />;
}
// TTFB results on Vercel:
// Pre-generated pages: ~15ms (served from edge cache)
// ISR cache hit: ~20ms (served from edge cache)
// ISR cache miss: ~200ms (SSR, then cached for future requests)
// On-demand revalidation: revalidatePath('/blog/my-post')
// app/api/revalidate/route.ts
// Trigger revalidation from CMS webhook
import { revalidatePath } from 'next/cache';
export async function POST(request: Request) {
const { secret, path } = await request.json();
if (secret !== process.env.REVALIDATION_SECRET) {
return Response.json({ error: 'Invalid secret' }, { status: 401 });
}
revalidatePath(path);
return Response.json({ revalidated: true, path });
}
// CMS publishes new content -> webhook triggers revalidation
// -> Vercel regenerates the page -> Next visitor gets fresh content
// Total TTFB impact: 0ms (revalidation happens in background)
Enable Edge Runtime for latency-sensitive routes
Vercel serverless functions run in a single region by default. For API routes or server-rendered pages that need low latency globally, switch to Edge Runtime. Edge functions execute on Vercel's edge network (powered by V8 isolates), eliminating the network round trip to a single origin region. The tradeoff: Edge Runtime has a smaller API surface (no Node.js filesystem, limited npm compatibility).
// app/api/search/route.ts
// Runs on Vercel's edge network (~30ms TTFB globally)
export const runtime = 'edge';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const q = searchParams.get('q') || '';
// Use edge-compatible data sources
const results = await fetch(
\`\${process.env.API_URL}/search?q=\${encodeURIComponent(q)}\`,
{ next: { revalidate: 300 } } // Cache for 5 min
);
return Response.json(await results.json(), {
headers: {
'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600',
},
});
}
// TTFB comparison (user in London):
// Serverless (us-east-1): ~380ms
// Edge Runtime: ~35ms
// middleware.ts
// Runs on every request at the edge before reaching your app
import { NextResponse } from 'next/server';
export function middleware(request) {
const response = NextResponse.next();
// Add cache headers for public HTML pages
const path = request.nextUrl.pathname;
if (!path.startsWith('/api/') && !path.startsWith('/dashboard/')) {
response.headers.set(
'Cache-Control',
'public, s-maxage=3600, stale-while-revalidate=86400'
);
}
return response;
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};
Configure function regions for your audience
By default, Vercel serverless functions deploy to iad1 (Washington, D.C.). If most of your users are in Europe or Asia, this adds 100-300ms of network latency on every SSR request. You can configure the function region in your project settings or in vercel.json to deploy closer to your users or your database.
{
"regions": ["iad1"],
"functions": {
"app/api/**/*.ts": {
"maxDuration": 10
}
}
}
// To change the default function region:
// Vercel Dashboard -> Project Settings -> Functions
// -> Serverless Function Region
//
// Choose the region closest to your database:
// iad1 (Washington D.C.) -- for US East databases
// sfo1 (San Francisco) -- for US West databases
// cdg1 (Paris) -- for EU databases
// hnd1 (Tokyo) -- for Asia-Pacific databases
//
// For global traffic with Edge Runtime:
// No region config needed (runs everywhere automatically)
// app/products/[id]/page.tsx
// Fetch all data in parallel to minimize function execution time
export default async function ProductPage({ params }) {
// BAD: Sequential fetching (each waits for the previous)
// const product = await getProduct(params.id); // 100ms
// const reviews = await getReviews(params.id); // 150ms
// const related = await getRelatedProducts(params.id); // 80ms
// Total: 330ms
// GOOD: Parallel fetching (all run simultaneously)
const [product, reviews, related] = await Promise.all([
getProduct(params.id), // 100ms
getReviews(params.id), // 150ms
getRelatedProducts(params.id) // 80ms
]);
// Total: 150ms (the slowest single fetch)
return <ProductView product={product} reviews={reviews} related={related} />;
}
Eliminate cold starts with Vercel cron and warm-up
Serverless functions on Vercel spin down after a period of inactivity. The first request after spin-down triggers a cold start: downloading the function bundle, initializing the runtime, and establishing database connections. This adds 500-2000ms to TTFB. You can mitigate cold starts with function warming, smaller bundles, and connection pooling.
{
"crons": [
{
"path": "/api/warm",
"schedule": "*/5 * * * *"
}
]
}
// app/api/warm/route.ts
// Lightweight endpoint that keeps the function warm
export async function GET() {
// Optionally warm database connection pool
await db.$connect();
return Response.json({ status: 'warm', timestamp: Date.now() });
}
// This pings your function every 5 minutes
// preventing cold starts for most traffic patterns
// lib/db.ts
// Use connection pooling to avoid reconnection overhead on every invocation
import { PrismaClient } from '@prisma/client';
// In serverless: reuse the client across warm invocations
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const db = globalForPrisma.prisma || new PrismaClient({
datasources: {
db: {
// Use Prisma Accelerate or PgBouncer for connection pooling
url: process.env.DATABASE_URL, // Should point to pooler
},
},
});
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
// Without pooling: each cold start opens a new DB connection (~100ms)
// With pooling: connections are reused across invocations (~5ms)
Optimize Vercel build output and bundle size
Smaller function bundles mean faster cold starts and faster execution. Vercel automatically tree-shakes your Next.js output, but heavy dependencies, unnecessary server-side imports, and large node_modules can bloat your function bundles. Use the Vercel function size analyzer to identify bloat.
// next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
// Enable standalone output for smaller deployments
output: 'standalone',
// Reduce bundle size with external packages
experimental: {
serverComponentsExternalPackages: [
'sharp', // Image processing (large binary)
'@prisma/client', // DB client
],
},
// Optimize images through Vercel's image service
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200],
imageSizes: [16, 32, 48, 64, 96, 128, 256],
},
// Headers for static assets
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'X-DNS-Prefetch-Control',
value: 'on',
},
],
},
];
},
};
// Check function sizes: vercel inspect <deployment-url>
// Target: < 1MB per function for fast cold starts
# After deploying, inspect function sizes
npx vercel inspect https://your-app.vercel.app
# Output shows each function's size:
# /api/search -- 245KB (good)
# /api/export -- 4.2MB (too large, will cold start slowly)
# /blog/[slug] -- 890KB (acceptable)
# Reduce large functions by:
# 1. Moving heavy deps to dynamic imports
# 2. Using edge runtime where possible (smaller V8 bundles)
# 3. Splitting monolithic API routes into focused endpoints
Quick checklist
- Content pages use ISR or SSG with appropriate revalidation intervals
- Edge Runtime enabled for latency-sensitive API routes
- Function region configured to match your database location
-
stale-while-revalidateheaders set via middleware - Database connection pooling configured (Prisma Accelerate or PgBouncer)
- Cron job warming critical serverless functions every 5 minutes
-
Function bundle sizes under 1MB (check with
vercel inspect)
Frequently asked questions
The most common cause is using SSR (server-side rendering) when ISR or static generation would work. SSR runs your page code on every request, while ISR caches the result and serves it from the edge. Check your route segments -- if they do not use export const dynamic = 'force-dynamic' but still render slowly, you may have a slow data fetch in a server component. Use export const revalidate = 60 to enable ISR caching.
Serverless functions run full Node.js in a single region with cold starts. Edge functions run on V8 isolates across 30+ global locations with near-zero cold starts but a smaller API surface (no Node.js filesystem access, limited npm packages). Use Edge for lightweight API routes, redirects, and A/B testing. Use Serverless for heavy computation, database queries with ORMs, and anything requiring Node.js APIs.
Three strategies: (1) Use a cron job to ping your functions every 5 minutes to keep them warm. (2) Reduce function bundle size by externalizing large packages and using dynamic imports. (3) Switch to Edge Runtime for functions that do not need full Node.js -- Edge functions have near-zero cold start time because V8 isolates spin up in under 5ms.
Vercel Pro offers longer function execution limits (60s vs 10s) and more bandwidth, but the underlying infrastructure is the same edge network. TTFB improvements come from configuration, not plan tier. However, Pro enables advanced features like Vercel Analytics with real-user TTFB monitoring, which helps you identify and fix slow pages more effectively.
Vercel KV (Redis-based) is excellent for caching and session data because it runs at the edge with sub-10ms latency. For primary data, use a database in the same region as your functions (Neon, PlanetScale, or Supabase all offer serverless-friendly options). The key is minimizing network hops: function and database in the same region, with KV at the edge for hot data.
Related resources
Complete TTFB Guide
Deep dive into Time to First Byte -- thresholds, measurement, and optimization.
FixFix TTFB on Netlify
Netlify-specific TTFB optimizations with edge functions and caching.
FixEdge Functions for TTFB
How to use edge computing to achieve sub-100ms TTFB globally.
FixServer Response TTFB
General server-side optimization strategies for faster TTFB.