EdgeCases Logo
Apr 2026
Next.js
Deep
9 min read

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.

nextjs
middleware
performance
edge-runtime
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 modules

Pattern 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 execution

Pattern 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

Related Insights

Explore related edge cases and patterns

Next.js
Surface
Next.js 16: Dynamic by Default, Turbopack Stable, proxy.ts
8 min
Next.js
Deep
proxy.ts: Node.js Runtime for Next.js Request Interception
8 min

Advertisement