Middleware läuft bei jeder Anfrage, auch bei denen, die Sie vergessen haben
Middleware ist der einzige Code in einer Next.js-Anwendung ohne Opt-out. Ein etwas zu breiter Matcher setzt sie vor jedes Bild, jede Schrift und jeden Prefetch - die Rechnung kommt als Latenz.
Jeder andere Code in einer Next.js-Anwendung läuft, weil etwas ihn erreicht hat. Middleware läuft, weil eine Anfrage stattgefunden hat. Es gibt keine Route, die sich abmeldet, keine Komponentengrenze, die sie eingrenzt, und keinen Cache davor.
Genau diese Eigenschaft macht sie für Auth und Locale-Routing nützlich - und macht einen unachtsamen Matcher zu einem der teuersten Fehler, die das Framework zulässt.
Was der Standard-Matcher wirklich trifft
Viele Codebasen tragen das hier, aus einem Tutorial kopiert:
export const config = {
matcher: '/:path*',
};Das ist jede Anfrage. Nicht jede Seite - jede Anfrage. Das Stylesheet. Jede
Schriftdatei. Jede Bildvariante, die next/image erzeugt. Jede
.rsc-Payload, die der Router vorlädt, sobald ein Link ins Sichtfeld
scrollt. Auf einer Seite mit zwanzig Links und einem Dutzend Bildern kann eine
Navigation sechzig Middleware-Aufrufe bedeuten, und achtundfünfzig davon
hatten nichts zu entscheiden.
Der Matcher, den man fast immer will, schließt die Maschinerie aus:
export const config = {
matcher: [
/*
Alles außer: den eigenen Assets des Frameworks, dem Bildoptimierer,
Dateien mit Endung und den Metadaten-Routen. Als negativer Lookahead
geschrieben, weil der Matcher zur Bauzeit zu einer Regex kompiliert und
ausgewertet wird, bevor irgendein Code von Ihnen läuft.
*/
'/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|.*\\..*).*)',
],
};Messen Sie vorher und nachher. In einem Audit brachte allein diese Änderung die Middleware-Aufrufe auf der Startseite von 71 auf 4.
Prefetch ist das, was übersehen wird
<Link> lädt standardmäßig vor, sobald er ins Sichtfeld kommt. Jeder Prefetch
ist eine echte Anfrage nach der RSC-Payload der Route, und jede geht durch die
Middleware.
Ein Footer mit dreißig Links sind also dreißig Middleware-Läufe in dem Moment, in dem ein Besucher nach unten scrollt - für Seiten, die er vielleicht nie öffnet. Wenn Ihre Middleware eine Datenbankabfrage macht oder einen Auth-Dienst ruft, haben Sie gerade das Scrollen teuer gemacht.
Die Regel ist einfach und durchsetzenswert: Middleware macht kein I/O. Cookie lesen, Signatur lokal prüfen, umleiten. Alles, was die Datenbank braucht, gehört in die Seite oder die Action, wo es einmal pro tatsächlich gestellter Anfrage läuft.
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 {
// Lokale Signaturprüfung - kein Netzwerk. Die Claims reichen, um zu
// entscheiden, ob die Anfrage durchdarf; ob dieser Nutzer DIESE
// Ressource sehen darf, ist die Frage der Seite, mit der Datenbank davor.
await jwtVerify(token, secret);
return NextResponse.next();
} catch {
return NextResponse.redirect(new URL('/login', request.url));
}
}Autorisierung in der Middleware ist ein Tor, keine Garantie
Middleware ist ein guter Ort, um anonyme Besucher zur Login-Seite zu schicken. Sie ist der falsche und einzige Ort, um zu entscheiden, ob dieser Nutzer diesen Datensatz lesen darf.
Zwei Gründe. Sie sieht Routenparameter nicht in strukturierter Form, also wird "darf dieser Nutzer Rechnung 4182 lesen" zur String-Zerlegung. Und Server Actions und Route Handler lassen sich direkt aufrufen - eine Prüfung, die nur in der Middleware lebt, ist eine Prüfung, die der Endpunkt nicht hat. Dieselbe Falle wie bei den drei üblichen Fehlern mit Server Actions.
In der Middleware das Tor, an der Datenzugriffsstelle die Autorisierung. Beides, nicht eines von beidem.
Der Runtime-Unterschied, den niemand erwähnt, bis es bricht
Middleware läuft in einer eingeschränkten Runtime. Kein fs, kein net,
keine nativen Node-Module, und ein Größenlimit für das Bundle. Ihr ORM dort zu
importieren warnt Sie nicht - es scheitert beim Build oder, schlimmer, beim
Deploy.
Es heißt auch, dass das Middleware-Bundle von Ihren Route-Bundles getrennt ist. Eine große Datumsbibliothek für einen einzigen Formatierungsaufruf hineinzuziehen legt diese Bibliothek in den Pfad jeder Anfrage, nicht auf eine Seite.
Was wir prüfen
- Was trifft der Matcher tatsächlich? Zählen Sie die Aufrufe bei einem echten Seitenaufruf; lesen Sie die Regex nicht und nehmen Sie an.
- Macht sie I/O? Wenn ja, zahlt diese Kosten jede Anfrage, Prefetches inklusive.
- Wird irgendeine Autorisierungsentscheidung nur hier getroffen?
- Was steckt im Bundle, und muss es das?
Middleware, die ein Cookie liest und umleitet, kostet Mikrosekunden und lohnt sich. Middleware, die einen Dienst ruft, kostet einen Roundtrip bei jeder Asset-Anfrage - und Teams entdecken das meist als "die Seite fühlt sich langsam an", ohne eine Route, der man es anlasten könnte.
Die andere Hälfte dieses Bildes ist, was mit ihr passiert, wenn Sie die Plattform verlassen und die Middleware statt hundert Edge-Standorten Ihr eines Serverzimmer ist: Next.js selbst hosten - und was dabei aufhört zu funktionieren.
