Vercel Application Optimization
A senior Vercel platform engineer who optimizes deployed applications for cost, performance, and reliability — mitigating cold starts, tuning CDN caching, optimizing Edge Functions, reducing bundle size, and setting up monitoring dashboards.
TL;DR Checklist
- Audit performance baseline with Vercel Analytics or Lighthouse CI before optimizing
- Mitigate cold starts by migrating latency-sensitive endpoints to Edge Functions
- Configure CDN caching with
stale-while-revalidatefor dynamic content andimmutablefor static assets - Split bundles at route level with
next/dynamicand lazy-load heavy dependencies - Optimize images with next/image configuration (formats, sizes, remotePatterns, loader)
- Use Next.js Script component with appropriate strategy for third-party scripts
- Set up Vercel Analytics, Speed Insights, and Logs for ongoing monitoring
- Track per-function execution cost to identify expensive operations
When to Use
Use this skill when:
- A Next.js app on Vercel has slow page loads or high Time-to-First-Byte (TTFB)
- Serverless function cold starts are noticeable (response times >500ms on first request)
- The monthly Vercel bill is growing faster than traffic
- Lighthouse scores are below 90 for Performance
- Images are unoptimized (large file sizes, wrong formats, missing dimensions)
- Third-party scripts block page rendering (no strategy-based loading)
- You need to implement ISR (Incremental Static Regeneration) with on-demand revalidation
When NOT to Use
Avoid this skill for:
- Apps not deployed on Vercel (different platform, different optimization strategies)
- Static sites with zero server-side computation — Vercel's defaults are already optimal
- Prototyping or staging environments where optimization overhead isn't justified
- Before establishing performance baselines — always measure the problem first
Core Workflow
Audit Performance — Establish baseline metrics:
- Enable Vercel Analytics and Speed Insights in the project dashboard
- Run Lighthouse CI on critical pages and record LCP, CLS, INP scores
- Identify pages with high TTFB (above 500ms), large bundle sizes, or unoptimized images
- Check Vercel Logs for function execution duration and error rates Checkpoint: Document baseline values for every metric you plan to improve. Without baselines, you cannot measure success.
Address Cold Starts — Reduce serverless function latency:
- Migrate latency-sensitive endpoints from Serverless Functions to Edge Functions (sub-50ms start)
- Implement lambda warmers for Serverless Functions that cannot be migrated
- Use
keep-aliveconnections to reuse database and API connections across invocations - Decrease function memory allocation if underutilized (cold starts scale with memory) Checkpoint: Verify that cold start latency drops below 200ms for critical paths. Retest from a fresh region.
Optimize Caching Strategy — Reduce origin requests:
- Configure
Cache-Controlheaders withs-maxage+stale-while-revalidatefor dynamic pages - Set
immutablewithmax-age=31536000for static assets (fonts, images, compiled CSS) - Implement ISR with appropriate
revalidateintervals and on-demand revalidation webhooks - Use
next.config.jsheadersfor global cache policies Checkpoint: Verify cache hit rate in Vercel Analytics is above 80% for static assets and above 50% for dynamic pages.
- Configure
Reduce Bundle Size — Ship less JavaScript:
- Use
next/dynamicwithssr: falsefor heavy client-only components (charts, maps, editors) - Tree-shake unused exports from large dependencies (lodash, moment, date-fns)
- Analyze bundle with
@next/bundle-analyzerand identify large chunks - Lazy-load below-the-fold components and route segments
- Replace large dependencies with lighter alternatives
Checkpoint: Run
ANALYZE=true next build— total JS per page must be under 150KB (gzipped) for pages above the fold.
- Use
Set Up Monitoring — Ensure ongoing visibility:
- Configure Vercel Analytics for real-user monitoring (RUM)
- Set up Speed Insights for Core Web Vitals tracking (LCP, CLS, INP)
- Export Vercel Logs and create custom metrics for function duration, error rate, and cache hit ratio
- Set up budget alerts: alert when LCP exceeds 2.5s or error rate exceeds 1% Checkpoint: Verify dashboards are populated with data within 24 hours of enabling.
Review Cost Metrics — Optimize the Vercel bill:
- Identify top-spending Serverless Functions by execution duration and invocation count
- Migrate high-traffic Serverless Functions to Edge Functions or ISR to reduce compute cost
- Optimize
next/imageconfiguration to reduce image optimization bandwidth charges - Review function memory allocation — over-allocated memory costs more per invocation Checkpoint: Compare current projected monthly cost vs. previous month — target 20% reduction.
Implementation Patterns
Pattern 1: Edge Function Migration (Cold Start Mitigation)
// ❌ BAD: Serverless Function for a simple API proxy — 500ms+ cold start
// pages/api/proxy.ts (Serverless Function)
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
res.status(200).json(data);
}
// ✅ GOOD: Edge Function for the same proxy — sub-50ms cold start
// app/api/proxy/route.ts (Edge Function — works in App Router)
export const runtime = 'edge';
export async function GET(request: Request) {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return Response.json(data);
}
// ✅ GOOD: Keep-alive connection reuse for database-backed endpoints
// lib/db.ts — reuse connection across invocations
let dbClient: DatabaseClient | null = null;
export async function getDb(): Promise<DatabaseClient> {
if (!dbClient) {
dbClient = await createClient({
connectionString: process.env.DATABASE_URL!,
poolSize: 1,
// Keep connection alive between function invocations
idleTimeoutMillis: 60_000,
});
}
return dbClient;
}
// app/api/users/route.ts — uses the cached connection
export const runtime = 'nodejs';
export async function GET() {
const db = await getDb(); // Reuses connection on warm invocations
const users = await db.query('SELECT * FROM users LIMIT 10');
return Response.json(users);
}
Pattern 2: Advanced Caching Strategy
// next.config.js — global cache headers
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
// Static assets: cache forever in CDN and browser
source: '/:path*.(svg|png|jpg|jpeg|gif|webp|woff2|ttf|eot|css|js)',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
{
// API routes: CDN cache with stale-while-revalidate
source: '/api/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, s-maxage=60, stale-while-revalidate=600',
},
],
},
{
// HTML pages: short CDN cache, fallback to server
source: '/:path((?!api|_next|static).*)',
headers: [
{
key: 'Cache-Control',
value: 'public, s-maxage=10, stale-while-revalidate=59',
},
],
},
];
},
};
// ✅ GOOD: ISR with on-demand revalidation
// app/blog/[slug]/page.tsx
interface BlogPageProps {
params: { slug: string };
}
export default async function BlogPost({ params }: BlogPageProps) {
const post = await getPost(params.slug);
return <article>{/* ... */}</article>;
}
// Revalidate every 60 seconds, but also trigger on-demand when content changes
export const revalidate = 60;
// app/api/revalidate/route.ts — on-demand revalidation webhook
export async function POST(request: Request) {
const { secret, slug } = await request.json();
// Validate secret to prevent unauthorized revalidation
if (secret !== process.env.REVALIDATION_SECRET) {
return Response.json({ message: 'Invalid secret' }, { status: 401 });
}
try {
await revalidatePath(`/blog/${slug}`);
return Response.json({ revalidated: true });
} catch (err) {
return Response.json({ message: 'Revalidation failed' }, { status: 500 });
}
}
Pattern 3: Bundle Optimization with next/dynamic
// ❌ BAD: Heavy chart library imported eagerly — adds 120KB to the main bundle
import { LineChart } from 'recharts';
export default function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button => setShowChart(true)}>Show Chart</button>
{showChart && (
<LineChart width={600} height={300} data={data}>
{/* ... */}
</LineChart>
)}
</div>
);
}
// ✅ GOOD: Chart library lazy-loaded — added to a separate chunk, loaded on demand
import dynamic from 'next/dynamic';
const LazyLineChart = dynamic(
() => import('recharts').then((mod) => mod.LineChart),
{
ssr: false, // Charts are client-only — no SSR needed
loading: () => <div className="chart-skeleton h-[300px] w-[600px] bg-gray-100 animate-pulse" />,
}
);
export default function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button => setShowChart(true)}>Show Chart</button>
{showChart && <LazyLineChart width={600} height={300} data={data} />}
</div>
);
}
// ✅ GOOD: Route-level code splitting with Next.js App Router
// app/dashboard/page.tsx — automatically code-split at route boundaries
export default function DashboardPage() {
return (
<main>
<h1>Dashboard</h1>
{/* Heavy components loaded per-route, not in the global layout */}
</main>
);
}
// app/analytics/page.tsx — separate chunk, no impact on dashboard load time
export default function AnalyticsPage() {
return (
<main>
<h1>Analytics</h1>
</main>
);
}
Pattern 4: Image Optimization with next/image
import Image from 'next/image';
// ❌ BAD: Unoptimized image — no sizes, wrong format fallback, large dimensions
function BadHero() {
return (
<img
src="/hero.png" // Large PNG — no WebP, no AVIF
alt="Hero"
style={{ width: '100%', height: 'auto' }}
/>
);
}
// ✅ GOOD: Fully optimized with next/image
function OptimizedHero() {
return (
<Image
src="/hero.jpg"
alt="Hero"
width={1920}
height={1080}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 50vw"
priority // LCP element — load immediately, don't lazy-load
quality={85} // Balance quality vs. file size
/>
);
}
// ✅ GOOD: next.config.js with remote image configuration
// next.config.js
const nextConfig = {
images: {
formats: ['image/avif', 'image/webp'], // AVIF preferred, WebP fallback
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
minimumCacheTTL: 60 * 60 * 24 * 30, // Cache optimized images for 30 days
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'cdn.example.com',
pathname: '/images/**',
},
],
},
};
export default nextConfig;
Pattern 5: Script Loading Strategies
// ❌ BAD: Script tag blocks rendering — no strategy, loads synchronously
export default function MarketingPage() {
return (
<div>
<script src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID" />
<script dangerouslySetInnerHTML={{
__html: `window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);}`
}} />
{/* Main content — blocked until scripts load */}
</div>
);
}
// ✅ GOOD: Next.js Script component with appropriate strategies
import Script from 'next/script';
export default function OptimizedMarketingPage() {
return (
<>
{/* Critical analytics — load before page becomes interactive, but don't block render */}
<Script
src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"
strategy="afterInteractive"
/>
{/* Chat widget — load after everything else is done */}
<Script
src="https://widget.intercom.io/widget/abc123"
strategy="lazyOnload"
/>
{/* Heatmap tool — not needed on mobile */}
<Script
src="https://static.hotjar.com/c/hotjar-123456.js"
strategy="lazyOnload"
/>
{/* Main page content renders immediately — not blocked by scripts */}
<main>
<h1>Welcome</h1>
<p>This content renders before any third-party scripts load.</p>
</main>
</>
);
}
Constraints
MUST DO
- Measure performance baselines with Vercel Analytics or Lighthouse CI before and after each optimization
- Use
stale-while-revalidatecaching for dynamic content withs-maxageset to acceptable freshness TTL - Set
Cache-Control: public, max-age=31536000, immutablefor all static assets (fonts, images, compiled CSS/JS) - Use
runtime: 'edge'for API routes that need sub-50ms cold starts and low latency globally - Use
next/dynamicwithssr: falsefor heavy client-only components that are not visible above the fold - Configure
next/imagewithformats: ['image/avif', 'image/webp']and appropriateremotePatterns - Use Next.js
<Script>component with explicitstrategy— never plain<script>tags - Monitor Serverless Function execution durations and migrate expensive ones to Edge or ISR
MUST NOT DO
- Use Serverless Functions for high-traffic, latency-sensitive endpoints — prefer Edge Functions or ISR
- Set
revalidate: 0on ISR pages that don't need real-time freshness — set a reasonable TTL - Include heavy npm packages (chart libraries, moment.js, lodash) in the main bundle — dynamic import them
- Use
<img>tags for anything that is not a user-uploaded image with unknown dimensions - Block page rendering with third-party scripts — always use
afterInteractiveorlazyOnloadstrategy - Ignore Vercel Analytics data — it reveals real user performance, not just synthetic Lighthouse scores
- Optimize without cost awareness — a 10ms function improvement isn't worth 2x memory allocation
Related Skills
| Skill | Purpose |
|---|---|
react-native-rendering |
Extends rendering optimization principles to React Native mobile apps |
Live References
Authoritative documentation links for Vercel deployment optimization.
- Vercel Performance Guide — Official performance overview
- Vercel Edge Functions Docs — Edge Function configuration and limits
- Vercel Caching & CDN — CDN cache behavior and Cache-Control headers
- Next.js Image Optimization — next/image configuration reference
- Next.js Script Component — Script loading strategies
- Vercel Analytics (RUM) — Real-user monitoring setup
- Vercel Speed Insights — Core Web Vitals tracking
- @next/bundle-analyzer — Bundle composition analysis
- Vercel Cost Optimization — Managing and reducing Vercel spend
- ISR with On-Demand Revalidation — ISR revalidation patterns