El middleware corre en cada petición, incluidas las que olvidaste
El middleware es el único código de una app Next.js sin forma de excluirse. Un matcher demasiado amplio lo pone delante de cada imagen, fuente y prefetch, y la factura llega como latencia.
Cualquier otro trozo de código de una aplicación Next.js corre porque algo llegó hasta él. El middleware corre porque hubo una petición. No hay ruta que se excluya, ni frontera de componente que lo contenga, ni caché por delante.
Esa propiedad es lo que lo hace útil para autenticación y enrutado por idioma, y es también por lo que un matcher descuidado es uno de los errores más caros que ofrece el framework.
Qué captura de verdad el matcher por defecto
Muchísimas bases de código llevan esto, copiado de un tutorial:
export const config = {
matcher: '/:path*',
};Eso es cada petición. No cada página: cada petición. La hoja de estilos. Cada
fichero de fuente. Cada variante de imagen que genera next/image. Cada
payload .rsc que el router precarga cuando un enlace entra en pantalla. En
una página con veinte enlaces y una docena de imágenes, una navegación pueden
ser sesenta invocaciones del middleware, y cincuenta y ocho no tenían nada que
decidir.
El matcher que casi siempre quieres excluye la maquinaria:
export const config = {
matcher: [
/*
Todo menos: los assets propios del framework, el optimizador de
imágenes, los ficheros con extensión y las rutas de metadatos. Escrito
como lookahead negativo porque el matcher se compila a una regex en el
build y se evalúa antes de que corra nada de tu código.
*/
'/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|.*\\..*).*)',
],
};Mide antes y después. En una auditoría, este cambio por sí solo llevó las invocaciones del middleware en la portada de 71 a 4.
El prefetch es lo que se le escapa a todo el mundo
<Link> precarga por defecto cuando entra en el viewport. Cada precarga es
una petición real del payload RSC de la ruta, y cada una pasa por el
middleware.
Así que un footer con treinta enlaces son treinta ejecuciones del middleware en cuanto el visitante baja del todo, por páginas que quizá nunca abra. Si tu middleware consulta la base de datos o llama a un servicio de autenticación, acabas de encarecer el acto de hacer scroll.
La regla es simple y vale la pena imponerla: el middleware no hace I/O. Lee la cookie, verifica la firma en local, redirige. Todo lo que necesite la base de datos va en la página o en la action, donde corre una vez por una petición que el visitante hizo de verdad.
import { NextResponse, type NextRequest } from 'next/server';
import { jwtVerify } from 'jose';
const secret = new TextEncoder().encode(process.env.SESSION_SECRET);
export async function middleware(request: NextRequest) {
const token = request.cookies.get('session')?.value;
if (!token) return NextResponse.redirect(new URL('/login', request.url));
try {
// Comprobación local de firma, sin red. Los claims bastan para decidir si
// dejar pasar la petición; si este usuario puede ver ESTE recurso es
// pregunta de la página, con la base de datos delante.
await jwtVerify(token, secret);
return NextResponse.next();
} catch {
return NextResponse.redirect(new URL('/login', request.url));
}
}Autorizar en el middleware es una puerta, no una garantía
El middleware es un buen sitio para mandar a un visitante anónimo a la página de login. Es el sitio equivocado -y el único- para decidir si este usuario puede leer este registro.
Dos razones. No ve los parámetros de ruta de forma estructurada, así que "¿puede este usuario leer la factura 4182?" se convierte en un ejercicio de parsear cadenas. Y las Server Actions y los Route Handlers se pueden llamar directamente, así que una comprobación que solo vive en el middleware es una comprobación que el endpoint no tiene: la misma trampa que los tres errores de siempre con las Server Actions.
Puerta en el middleware, autorización en el acceso a datos. Las dos, no una.
La diferencia de runtime que nadie menciona hasta que rompe
El middleware corre en un runtime limitado. Sin fs, sin net, sin módulos
nativos de Node, y con un límite de tamaño para el bundle. Importar ahí tu ORM
no te avisa: falla en el build o, peor, en el despliegue.
También significa que el bundle del middleware es independiente del de tus rutas. Meter una librería de fechas grande para una sola llamada de formato añade esa librería al camino de cada petición, no al de una página.
Lo que comprobamos
- ¿Qué captura realmente el matcher? Cuenta invocaciones en una carga real; no leas la regex y supongas.
- ¿Hace I/O? Si sí, ese coste lo paga cada petición, precargas incluidas.
- ¿Hay alguna decisión de autorización que se tome solo aquí?
- ¿Qué hay en el bundle, y hace falta que esté?
Un middleware que lee una cookie y redirige cuesta microsegundos y vale la pena. Uno que llama a un servicio cuesta un viaje de ida y vuelta en cada petición de asset, y los equipos suelen descubrirlo como "el sitio va lento" sin ninguna ruta a la que culpar.
La otra mitad del cuadro es qué le pasa cuando dejas la plataforma, donde el middleware deja de ser cien ubicaciones en el edge y pasa a ser tu único servidor: autoalojar Next.js y qué deja de funcionar.
