Next.js Middleware Performance
Middleware runs on every request - make it fast. Learn Edge vs Node runtime trade-offs, header mutation pitfalls, and rewrite rule optimization.
Next.js middleware runs on every request, making it a performance bottleneck if misused. Understanding when middleware runs on Edge vs Node runtime, avoiding header mutations that trigger full revalidations, and optimizing rewrite rules is critical for production apps.
The Problem: Middleware Runs Everywhere
Middleware executes on every route match, including:
- Static pages (even cached ones)
- API routes
- Image optimization requests
- Static assets (if matcher includes them)
This means a 5ms middleware delay becomes 5ms on every request.
Pattern 1: Edge vs Node Runtime Selection
Middleware defaults to Edge Runtime, which has trade-offs:
// middleware.ts - Defaults to Edge Runtime
export const runtime = 'edge'; // Fast cold starts, limited Node APIs
export async function middleware(request: NextRequest) {
// Edge Runtime limitations:
// - No fs, path, crypto (subset available)
// - Limited environment variables
// - No native modules
}When to use Edge vs Node:
// Edge Runtime: Fast cold starts, low latency
export const runtime = 'edge';
// Use for:
// - Geo-based routing
// - A/B testing
// - Simple auth checks
// - Header modifications
// Node Runtime: Slower cold starts, full Node APIs
export const runtime = 'nodejs';
// Use for:
// - Database queries
// - File system operations
// - Heavy crypto operations
// - Native modulesPattern 2: Minimize Middleware Scope
Use matcher to exclude unnecessary routes:
// Bad: Matches everything
export const config = {
matcher: '/:path*',
};
// Good: Exclude static assets and images
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico).*)',
],
};
// Better: Match only specific routes
export const config = {
matcher: [
'/dashboard/:path*',
'/api/:path*',
],
};Performance impact:
// Unoptimized middleware:
// Runs on 10,000 requests/day x 5ms = 50,000ms total
// Optimized middleware:
// Runs on 1,000 requests/day x 5ms = 5,000ms total
// 90% reduction in middleware executionPattern 3: Avoid Header Mutations on Static Pages
Adding custom headers in middleware breaks Next.js caching:
// Bad: Header mutations break caching
export async function middleware(request: NextRequest) {
const response = NextResponse.next();
// This header breaks Next.js caching
response.headers.set('x-custom-header', 'value');
return response;
}Next.js can't cache responses with custom headers. Static pages become dynamic:
// Good: Add headers only when needed
export async function middleware(request: NextRequest) {
const response = NextResponse.next();
// Add header only for API routes (already dynamic)
if (request.nextUrl.pathname.startsWith('/api')) {
response.headers.set('x-custom-header', 'value');
}
return response;
}Edge Case 1: Locale Detection Performance
Next.js i18n middleware adds locale to all requests:
// i18n middleware - Runs on EVERY request
export async function middleware(request: NextRequest) {
const locale = request.nextUrl.locale || request.headers.get('accept-language');
if (!request.nextUrl.pathname.startsWith('/' + locale)) {
return NextResponse.redirect(
new URL('/' + locale + request.nextUrl.pathname, request.url)
);
}
return NextResponse.next();
}Optimize locale detection:
// Fast locale detection
export async function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// Skip if already has locale
if (pathname.startsWith('/en/') || pathname.startsWith('/es/')) {
return NextResponse.next();
}
// Detect locale from header (fast)
const acceptLanguage = request.headers.get('accept-language') || '';
const locale = acceptLanguage.startsWith('es') ? 'es' : 'en';
// Skip for static assets (matcher handles this)
// Skip for API routes (no locale needed)
if (pathname.startsWith('/api')) {
return NextResponse.next();
}
return NextResponse.redirect(
new URL('/' + locale + pathname, request.url)
);
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|api|favicon.ico).*)',
],
};Edge Case 2: Cookie Parsing Overhead
Parsing cookies in middleware is expensive:
// Bad: Parse cookies on every request
export async function middleware(request: NextRequest) {
const cookies = request.cookies;
// Expensive: Parses all cookies
const session = cookies.get('session');
const theme = cookies.get('theme');
const prefs = cookies.get('prefs');
// ... use cookies
}Parse only when needed:
// Good: Lazy cookie parsing
export async function middleware(request: NextRequest) {
const url = request.nextUrl;
// Check route first (fast string comparison)
if (!url.pathname.startsWith('/dashboard')) {
return NextResponse.next();
}
// Parse cookies only for protected routes
const session = request.cookies.get('session');
if (!session) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}Key Takeaways
- Runtime choice: Edge for cold-start speed, Node for full APIs
- Scope reduction: Use matcher to exclude static assets
- Header mutations: Avoid on static pages to preserve caching
- Locale detection: Skip for API routes and static assets
- Cookie parsing: Lazy parse only when needed
Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
Tools & Utilities
Related Insights
Explore related edge cases and patterns
Advertisement