Deploying Next.js Applications on Vercel: Production Optimization Guide
Master Vercel deployment for Next.js with edge functions, ISR, environment management, preview deployments, analytics, and production performance tuning.
Deploying Next.js Applications on Vercel: Production Optimization Guide
Vercel is the natural home for Next.js applications — built by the same team, with zero-config deployment, edge network distribution, and built-in analytics. But deploying is just the beginning. Getting the most out of Vercel requires understanding ISR, edge functions, environment management, and performance monitoring. This guide covers the production deployment patterns I use for portfolio sites and client applications.
Project Configuration
next.config.ts for Production
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
// Image optimization
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 768, 1024, 1280, 1536],
imageSizes: [16, 32, 48, 64, 96, 128, 256],
minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
},
// Security headers
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-XSS-Protection', value: '1; mode=block' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()',
},
],
},
];
},
// Redirects for SEO
async redirects() {
return [
{
source: '/home',
destination: '/',
permanent: true,
},
];
},
// Experimental optimizations
experimental: {
optimizePackageImports: ['react-icons', 'framer-motion', 'lodash-es'],
},
};
export default nextConfig;
vercel.json for Advanced Configuration
{
"framework": "nextjs",
"regions": ["iad1"],
"crons": [
{
"path": "/api/cron/revalidate",
"schedule": "0 */6 * * *"
}
],
"headers": [
{
"source": "/fonts/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
}
]
}
Environment Management
Environment Variable Strategy
# .env.local (development only, never committed)
GOOGLE_AI_API_KEY=dev_key_here
NEXT_PUBLIC_API_URL=http://localhost:3000/api
# .env.production (production values set in Vercel dashboard)
# GOOGLE_AI_API_KEY → Set in Vercel Environment Variables
# NEXT_PUBLIC_API_URL → Set in Vercel Environment Variables
Organize Vercel environment variables by scope:
| Variable | Production | Preview | Development |
|---|---|---|---|
GOOGLE_AI_API_KEY | Production key | Preview key | Dev key |
NEXT_PUBLIC_API_URL | https://api.example.com | https://preview.api.example.com | http://localhost:3000/api |
DATABASE_URL | Production DB | Preview DB | Local DB |
Critical rule: Never share production database credentials with preview deployments. Use separate databases or read-only replicas.
Incremental Static Regeneration (ISR)
On-Demand Revalidation
// app/api/revalidate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function POST(request: NextRequest) {
const authHeader = request.headers.get('Authorization');
if (authHeader !== `Bearer ${process.env.REVALIDATION_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { path, tag } = await request.json();
if (tag) {
revalidateTag(tag);
return NextResponse.json({ revalidated: true, tag });
}
if (path) {
revalidatePath(path);
return NextResponse.json({ revalidated: true, path });
}
return NextResponse.json({ error: 'Path or tag required' }, { status: 400 });
}
Time-Based Revalidation for Blog Posts
// app/blog/[slug]/page.tsx
export const revalidate = 3600; // Revalidate every hour
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return <BlogContent post={post} />;
}
Edge Functions
Middleware at the Edge
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export const config = {
matcher: ['/((?!_next|favicon.ico|api/public).*)'],
};
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// Geo-based content hints
const country = request.geo?.country || 'US';
response.headers.set('x-user-country', country);
// Bot detection for analytics
const userAgent = request.headers.get('user-agent') || '';
const isBot = /bot|crawl|spider|slurp|googlebot/i.test(userAgent);
if (isBot) {
response.headers.set('x-is-bot', 'true');
}
return response;
}
Edge API Routes for Low Latency
// app/api/edge/greeting/route.ts
export const runtime = 'edge';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const name = searchParams.get('name') || 'visitor';
return Response.json(
{ message: `Hello, ${name}! Welcome to my portfolio.` },
{
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
},
}
);
}
Preview Deployments
Every git branch gets a unique URL for review:
Branch-Based Workflows
main → production (your-domain.com)
staging → preview (staging-your-project.vercel.app)
feature/* → preview (feature-xyz-your-project.vercel.app)
Preview Comment Integration
Vercel automatically comments on GitHub PRs with:
- Preview URL
- Build logs
- Performance metrics
- Lighthouse scores
Protecting Preview Deployments
// vercel.json
{
"passwordProtection": {
"deploymentType": "preview"
}
}
Performance Monitoring
Vercel Analytics Integration
// app/layout.tsx
import { Analytics } from '@vercel/analytics/react';
import { SpeedInsights } from '@vercel/speed-insights/next';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Analytics />
<SpeedInsights />
</body>
</html>
);
}
Custom Performance Tracking
// lib/performance.ts
export function reportWebVitals(metric: {
id: string;
name: string;
value: number;
label: string;
}) {
// Send to your analytics endpoint
if (metric.label === 'web-vital') {
fetch('/api/analytics/vitals', {
method: 'POST',
body: JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
}),
headers: { 'Content-Type': 'application/json' },
});
}
}
Caching Strategy
Static Assets
// next.config.ts headers
{
source: '/_next/static/(.*)',
headers: [
{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }
]
}
API Response Caching
// app/api/projects/route.ts
export async function GET() {
const projects = await getProjects();
return Response.json(projects, {
headers: {
'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600',
},
});
}
Build Optimization
Analyzing Bundle Size
# Install analyzer
npm install @next/bundle-analyzer
# next.config.ts
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer(nextConfig);
# Run analysis
ANALYZE=true npm run build
Reducing Build Time
// next.config.ts
const nextConfig: NextConfig = {
// Skip type checking in CI (handled by separate job)
typescript: {
ignoreBuildErrors: process.env.CI === 'true',
},
// Skip linting in build (handled by separate job)
eslint: {
ignoreDuringBuilds: process.env.CI === 'true',
},
};
Key Takeaways
- Security headers in next.config.ts — X-Frame-Options, CSP, and Permissions-Policy
- Separate preview and production environments — Never share production credentials
- ISR for dynamic content — Time-based and on-demand revalidation
- Edge middleware for geo-targeting — Sub-millisecond response customization
- Vercel Analytics + Speed Insights — Built-in performance monitoring
- Cache aggressively — Static assets immutable, API responses with stale-while-revalidate
Vercel turns deployment from a chore into a competitive advantage. Every push gets a preview, every merge goes live, and every request is served from the edge closest to your user.