Zuerst messen: Warum frühe Optimierung Next.js-Apps schwieriger zum Ausführen macht
Sehen Sie, wie vorzeitige Memoisierung, weit gefasste Client-Grenzen sowie gestapelte Caches Komplexität in Next.js-Apps hinzufügen und wie ein auf Messungen ausgerichteter Arbeitsablauf dafür sorgt, dass sie schnell bleiben.
Eine Next.js-Seite lädt in 1,2 Sekunden – jemand hält das für zu langsam, und schon beginnt eine Optimierungsrunde, bevor überhaupt jemand ein Profil angezeigt hat. Ein paar Monate später enthält die Codebasis überall Memoisierung, dynamische Importe, die niemand getestet hat, mehrere überschneidende Caches sowie eigene Renderpfade, und die Anwendung ist schwieriger zu verstehen als je zuvor. Dieser Leitfaden erklärt, warum dieses Muster so verbreitet ist, welche konkreten Gewohnheiten dazu führen und wie man sie durch einen Arbeitsablauf ersetzen kann, der auf Fakten beruht und Komplexität nur dann hinzufügt, wenn die Zahlen dies rechtfertigen.
Der eigentliche Fehler: Komplexität vor Fakten
Jede einzelne Optimierung erscheint in einer Code-Review in der Regel sinnvoll. Ein useMemo hier, ein Cache dort, ein geteilter Bundle für eine komplex erscheinende Komponente. Das Problem ist jedoch kumulativ: Jeder Mechanismus fügt ein weiteres Caching-Verhalten hinzu, das verstanden werden muss, einen weiteren Render-Pfad zum Debuggen, eine weitere Grenze, über die nachgedacht werden muss, sowie mehr performancebezogenen Code, der weiterhin funktionieren soll.
Der Fehler, vor dem man sich schützen muss, ist daher nicht ein Mangel an Optimierungen. Es geht vielmehr darum, Mechanismen einzuführen, bevor man weiß, was langsam ist, warum es langsam ist und ob eine Behebung etwas verändert, was der Benutzer bemerkt. Bevor man die Ausführung optimiert, muss das System ausreichend beobachtbar sein, damit man es messen kann.
Profilieren, bevor man etwas ändert
Die häufigste Form dieses Fehlers besteht darin, auf die allgemeine Annahme zu reagieren, dass Leistung wichtig ist. Ein Entwickler beginnt mit der Bearbeitung von Code, ohne Profilierung durchzuführen, ohne Engpässe zu identifizieren und ohne sicherzustellen, dass die Benutzer auf etwas Bestimmtes warten.
Die Symptome sind vertraut:
useMemounduseCallback, die um teure Berechnungen gewickelt werden- Caching-Ebenen, die für Daten hinzugefügt werden, deren Abruf niemals aufwändig war
- Code-Splitting, das angewandt wird, bevor überprüft wurde, welche Teile besonders groß sind
- Abstraktionen, die auf hypothetischer zukünftiger Last basieren
- Render-Logik, die Bedingungen erweitert, um Render-Vorgänge zu vermeiden, die niemand gemessen hat
Oftmals existierte das ursprüngliche Problem kaum. Wahre Leistungsverbesserungen beginnen mit Daten: Seitenlaufzeiten, eine Bundle-Analyse, Aufnahmen eines React-Profilers, die Netzwerkstruktur sowie ein klares Bild davon, wo Nutzer tatsächlich warten. Die Optimierung sollte auf beobachtetem Verhalten beruhen, nicht auf vagen Befürchtungen darüber, was eines Tages langsam werden könnte.
Eine praktische Faustregel ist es, vor dem Anfassen des Codes die Metrik aufzuschreiben, die man verbessern möchte, sowie ihren aktuellen Wert. Wenn man die Zahl nicht benennen kann, ist man noch nicht bereit, die Implementierung zu ändern.
In der Regel liegt das Problem einfach daran, dass zu viel JavaScript verwendet wird
Viel Langsamkeit im Frontend ist nicht rätselhaft. Der Browser wird gebeten, mehr Skripte herunterzuladen, zu analysieren, zu kompilieren und auszuführen, als die Seite benötigt – und bei einem Mittelklasse-Handy ist jeder dieser Schritte aufwendig.
Eine schlichte Bedienoberfläche kann still und heimlich mehrere Animationsbibliotheken, ein umfangreiches Komponenten-Set, einen ressourcenintensiven Zustandsmanager, Diagrammierpakete, Hilfsfunktionen sowie Client-Seiten-Logik für Funktionen ansammeln, die eigentlich auf dem Server bleiben hätten können. Keines davon wirkt für sich genommen beunruhigend. Gemeinsam erzeugen sie jedoch eine große Menge an Arbeit, bevor die Seite angemessen auf Eingaben reagiert. Zu diesem Zeitpunkt beginnen Teams oft, verzweifelt nach besseren Lighthouse-Werten zu streben, als würde die Lösung eine clevere Strategie erfordern.
Next.js bietet bereits starke Standardeinstellungen: Server-Rendering, automatische Code-Splitting pro Route sowie ein serverbasiertes Modell, das auf React Server Components beruht. Diese Standardeinstellungen verlieren jedoch viel von ihrem Wert, wenn große Teile der Anwendung dennoch in den Browser geladen werden. Bevor man daher auf andere Techniken zurückgreift, sollte man prüfen, wie viel Code überhaupt transportiert wird, welche Abhängigkeiten jede Route dominieren und ob jede von ihnen ihren Platz verdient. Viele Anwendungen benötigen keine ausgefeilteren Optimierungen – sie brauchen einfach weniger JavaScript. Für eine detailliertere Betrachtung dessen, was neben der reinen Bundle-Größe eine Seite verlangsamen kann, siehe Erfassen, was eigentlich eine Web-Anwendung verlangsamt.
Kundenbereiche, die nach oben expandieren
Der schnellste Weg, die Vorteile des App Routers im Bereich Architektur zu verschleiern, besteht darin, zu viel als Client-Code zu markieren. Jemand benötigt Interaktivität tief in der Hierarchie, fügt daher "use client" zu einem Elternteil hinzu, anschließend zu einem Großeltern-Element – bald gehören Komponenten, die nur statische Markup-Strukturen rendern, Serverdaten lesen oder Layouts zusammenstellen, einfach deshalb zum Client-Bundle, weil sie unter dieser Anweisung liegen.
Diese Veränderung betrifft nicht nur den Ort, an dem der Code ausgeführt wird. In der Regel bedeutet sie:
- mehr JavaScript, das an den Browser gesendet wird
- mehr Aufwand für die Initialisierung, bevor die Seite interaktiv wird
- einen zusätzlichen Client-Seiten-Zustand, der verwaltet werden muss
- mehr Stellen, an denen Server- und Client-Daten aus dem Gleichgewicht geraten können
Darin liegt eine Ironie. Viele Teams wechseln ausgerechnet zu dem modernen Next.js, um eine serverbasierte Architektur zu erhalten, und bauen anschließend allmählich wieder die clientintensive Single-Page-App auf, die sie eigentlich hinter sich lassen wollten.
Eine nützliche Frage bei der Gestaltung ist: Welcher kleinste Bestandteil dieser Benutzeroberfläche benötigt tatsächlich den Browser? Ein Like-Button, ein Dropdown-Menü oder ein Formfeld mögen einen Client-Zustand erfordern; die darum herum liegenden Karten, Listen und Seiten oft nicht. Wenn man diese Grenzen klar definiert und möglichst serverseitig generierten Inhalt als Kinderkomponenten an Client-Komponenten weitergibt, kann der Server mehr Arbeit leisten, während die interaktiven Bereiche fokussiert bleiben. Die langfristige Vorteil ist eine kleinere Dateigröße sowie ein einfacheres Verständnis der Architektur. Die dahinterstehenden Mechanismen werden in „Wie React Server Components Code aus dem Bundle halten“ erläutert.
Wie frühzeitige Optimierung Architekturen anfällig macht
Leistungsverbesserungen werden zu Wartungsproblemen, wenn Optimierungen schneller eingeführt werden, als jemand nachweisen kann, dass sie tatsächlich helfen. Es beginnt meist klein: Ein Komponent wird gememorisert, es entsteht ein manuell erstellter Cache, ein Hook wird hinzugefügt, um eine Renderung zu überspringen, anschließend taucht eine weitere Schicht auf, um zwei Zustände konsistent zu halten. Zu diesem Zeitpunkt scheint nichts riskant zu sein.
Monate später enthält die Codebasis benutzerdefinierte Hooks, deren Wechselwirkungen schwer nachvollziehbar sind, Invaliderungsregeln, die nur wenige Personen verstehen, Ketten von gememoriserten Werten, Renderbedingungen, die auf nicht mehr gültigen Annahmen beruhen, sowie Synchronisierungscode, der hauptsächlich deshalb existiert, weil eine frühere Optimierung ihn erforderte.
Das hat tatsächlich Kosten. Die Einführung neuer Funktionen dauert länger, das Fehlersuchen erfordert mehr Kontext, und kleine Änderungen an Funktionen beeinflussen letztendlich Mechanismen, die ursprünglich zur Beschleunigung hinzugefügt wurden. Die richtige Frage bei jeder Optimierung ist nicht, ob sie eine Leistungsmetrik verbessert, sondern ob die Verbesserung groß genug ist, um die dadurch entstehenden architektonischen Kosten zu rechtfertigen. Das Ziel ist eine nachhaltige Leistung: Eine Anwendung, die schnell reagiert, während ihr Alltagscode einfach und lesbar bleibt – anstatt voller Tricks zur Beschleunigung zu sein.
Die wahrgenommene Leistung ist ein UX-Problem
Ingenieure neigen dazu, sich auf das zu konzentrieren, was präzise gemessen werden kann – wie Millisekunden, Dateigrößen, Anzahl der Renderungen und Bewertungen. Nutzer erleben hingegen etwas Umfassenderes. Ein Zeitvorteil von 100 Millisekunden bei der Seitenladezeit nützt wenig, wenn die Navigation verwirrend ist, keine Rückmeldung über den Ladezustand gegeben wird, die Steuerelemente unreaktiv erscheinen, das Layout bei Ankunft neuer Inhalte stark schwankt oder eine wichtige Aktion keinerlei Anzeichen dafür zeigt, dass sie registriert wurde.
Nehmen wir ein Formular, das zwei Sekunden zum Absenden benötigt. Die Reduzierung der Zeit im Backend auf 1,7 Sekunden stellt einen echten technischen Vorteil dar. Die Bereitstellung sofortiger Rückmeldungen, das Deaktivieren der Schaltfläche, um Doppelabsendungen zu verhindern, sowie die Anzeige eines klaren Fortschrittsbalkens verbessern in den meisten Fällen das Nutzererlebnis deutlich mehr – auch wenn die Anfrage genauso langsam wie zuvor ist.
Das ist die Lücke zwischen gemessener und wahrgenommener Leistung. Die Menschen beurteilen eine Benutzeroberfläche danach, ob sie auf ihre Absichten reagiert, ob sie verstehen, was vor sich geht, und ob sie sich stabil anfühlt, während sie damit arbeiten. Gute Arbeit bei der Optimierung der Frontend-Leistung stützt sich daher genauso auf das Interaktionsdesign wie auf die internen Renderungsmechanismen. Werkzeuge wie React-Übergänge, optimistische Aktualisierungen und Skelettzustände gehören genauso zum Werkzeugkasten wie die Analyse von Bundeln.
Caching hilft – bis niemand es mehr erklären kann
Caching kann große Vorteile bringen, weil das System aufwendige Arbeiten nicht mehr wiederholt. Die Schwierigkeiten beginnen, wenn das Team nicht mehr sagen kann, welche Datenversion ein bestimmter Benutzer sehen sollte.
Die typische Geschichte: Eine Seite wird schnell, danach erscheinen veraltete Daten. Ein Datensatz wird geändert und ein Bildschirm aktualisiert sich, während ein anderer weiterhin den alten Wert anzeigt. Die Entwicklung funktioniert korrekt, aber nicht in der Produktion – und die Untersuchung führt zu Fragen darüber, welcher Cache die Antwort geliefert hat, welche Schicht ungültig wurde und welche Anfrage die Ausgabe erzeugt hat.
Große Next.js-Anwendungen erleichtern dies besonders, da die Wiederverwendung auf vielen Ebenen möglich ist: Ihr eigener Code, das Caching von Daten und Routen des Frameworks, einzelne fetch-Aufrufe, ein CDN, der Browser sowie die Backend-Dienste. Das Hinzufügen einer weiteren Schicht ohne Verständnis dafür, wie diese miteinander interagieren, kann die Latenz verringern, während gleichzeitig die Anzahl der möglichen Zustände des Systems zunimmt. Beachten Sie, dass sich die Standardeinstellungen für das Caching in Next.js über die verschiedenen Hauptversionen geändert haben, weshalb Sie sich an der aktuellen Dokumentation über das Verhalten der von Ihnen genutzten Version informieren sollten, anstatt auf ältere Anleitungen zu vertrauen.
Die Beobachtbarkeit sollte Vorrang vor aggressivem Caching haben. Für jede gecachte Antwort sollte das Team in der Lage sein, folgende Fragen zu beantworten:
- woher die Antwort stammt
- wie lange sie voraussichtlich gültig bleibt
- was sie ungültig macht
- was passiert, wenn die Ungültigmachung fehlschlägt
Ein Cache, der das System beschleunigt, aber das Verhalten in der Produktion unvorhersehbar macht, ist kein echter Gewinn.
Die Langsamkeit liegt vielleicht gar nicht in React
Mannchmal wird eine Verzögerung erst am Frontend sichtbar, nicht dort, wo sie entsteht. Wenn eine Ansicht einige Sekunden braucht, bis sie nutzbar ist, beginnt ein Team möglicherweise damit, Render-Prozesse zu optimieren, Komponenten zu memoisieren oder den Client-Zustand umzustrukturieren. Solche Änderungen können zwar ein paar Millisekunden an Rechenzeit im Browser sparen, während die Seite weiterhin auf eine zweisekündige Datenbankabfrage oder einen Endpunkt wartet, der weitaus mehr Daten zurückgibt, als die Ansicht benötigt.
Stellen Sie sich den Lebenszyklus dieser Anfrage vor: Der Browser sendet sie, sie durchläuft Middleware und Autorisierungsverfahren, der Server ruft einen Backend-Server oder eine Datenbank auf, die Nutzdaten kehren zurück – erst danach rendernt React. Wenn der größte Teil der verstrichenen Zeit vor Erhalt der Antwort vergeht, hilft es kaum, den letzten Schritt etwas zu beschleunigen.
Das Gleiche gilt für überdimensionierte Nutzdaten, sequentielle Netzwerkaufrufe, die parallel ausgeführt werden könnten, aufwändige Autorisierungsprüfungen, überlastete Dienste sowie unindizierte Abfragen. Keines dieser Probleme lässt sich dadurch lösen, dass Komponenten seltener rendern werden. Eine nützliche Untersuchung verfolgt den gesamten Anfrageverlauf und fragt nach, wohin die Zeit geht. Engpässe berücksichtigen weder Teamgrenzen noch die Zugehörigkeit des Codes zu bestimmten Personen.
Einfache Systeme bleiben länger schnell
Viele schnelle Anwendungen sind im Inneren unbedeutend. Sie liefern relativ kleine Pakete, ziehen klare Darstellungsgrenzen, halten den Client-Zustand minimal, laden Daten vorhersehbar und weisen eine Architektur auf, der ein neuer Entwickler folgen kann, ohne ein Netzwerk an Tricks reverse-engineern zu müssen.
Diese Schlichtheit zahlt sich aus, wenn die Anwendung wächst. Wenn der Datenfluss offensichtlich ist, lassen sich aufwändige Aufgaben leicht erkennen. Wenn die Client-Grenzen eng sind, ist klar, wofür der Browser verantwortlich ist. Wenn es wenige und eindeutige Caching-Regeln gibt, sind Produktionsprobleme leichter zu diagnostizieren.
Kluge Optimierung ist attraktiv, weil sie zum einen technisches Können zeigt, doch jedes Mechanismus wird zu etwas, das zukünftige Entwickler verstehen, debuggen, beibehalten oder letztendlich entfernen müssen. Das spricht nicht gegen Optimierung – es spricht vielmehr für die einfachste Implementierung, die den tatsächlichen Leistungsanforderungen entspricht, wobei zusätzliche Komplexität nur dann hinzugefügt werden sollte, wenn Messungen zeigen, dass das einfache Design an seine Grenzen gestoßen ist. Ein etwas weniger kluges System, mit dem es viel einfacher ist umzugehen, hält in der Regel länger stand.
Betrachten Sie Werte als Signale, nicht als Ziele
Benchmark-Tools sind wertvoll, weil sie unsichtbare Eigenschaften sichtbar machen. Das Problem beginnt, wenn das Erhöhen von Werten wichtiger wird als die Verbesserung des Produkts.
Ein gutes Lighthouse-Ergebnis garantiert nicht automatisch ein gutes Interaktionsdesign, eine wartbare Architektur, zuverlässiges Verhalten in der Produktion oder eine schnelle Erledigung der für die Nutzer wichtigen Aufgaben. Labormessungen finden unter kontrollierten Voraussetzungen statt; echte Besucher verwenden unterschiedliche Geräte, Netzwerke, Datenvolumina, Authentifizierungsstatus und Navigationspfade. Genau deshalb sind Felddaten wie die aus echten Sitzungen gesammelten Core Web Vitals eine nützliche Ergänzung.
Das macht die Labormetriken nicht unwichtig; es ändert lediglich, wie man sie verwendet:
- Falls eine Metrik auf ein echtes Problem hinweist, sollte dieses untersucht werden.
- Falls eine Änderung die Punktzahl erhöht, aber erhebliche Komplexität hinzufügt, während die Nutzer fast nichts bemerken, sollte der Kompromiss in Frage gestellt werden.
- Jede Metrik sollte mit einem vom Nutzer wahrnehmbaren Verhalten verbunden sein, das beschrieben werden kann.
Ziel ist nicht eine App, die in Benchmarks hervorragend aussieht. Es geht um eine App, die es den Nutzern ermöglicht, ihre Arbeit ohne unnötige Verzögerungen oder Hindernisse zu erledigen.
Ein auf Messungen ausgerichteter Arbeitsablauf
Wenn man diese Ideen zusammenführt, sieht ein nachhaltiger Kreislauf so aus:
- Nennen Sie das für den Benutzer sichtbare Problem sowie die Kennzahl, die es darstellt.
- Messen Sie den aktuellen Wert sowohl im Labor als auch, wo möglich, unter Feldbedingungen.
- Verfolgen Sie den gesamten Anfragen- und Darstellungsweg, um die größte Kostenquelle zu finden.
- Probieren Sie zunächst Änderungen, die Arbeit reduzieren: weniger Abhängigkeiten, schmalere Client-Grenzen, kleinere Datenmengen, schnellere Abfragen.
- wenden Sie Memoisierung, zusätzliches Caching oder benutzerdefinierte Darstellung nur dann an, wenn eine Reduzierung nicht ausreicht.
- Messen Sie erneut und behalten Sie die Änderung nur bei, wenn der Gewinn die Wartungskosten rechtfertigt.
Die Client-Grenze gehört auf das Blatt
'use client' auf der Seite zieht den Datenabruf ins Browser-Bundle. Die Seite bleibt eine Server Component.
// app/dashboard/page.tsx — a Server Component, no 'use client'
import { Chart } from './chart';
export default async function Dashboard() {
const points = await loadSeries();
return <Chart points={points} />;
}
Memoisieren Sie das Blatt erst, wenn ein Profil zeigt, dass das Rendern teuer ist.
'use client';
export const Chart = ({ points }: { points: number[] }) => {
return <svg data-count={points.length} />;
};
Haupterkenntnisse
- Der teuerste Leistungsfehler bei Next.js besteht darin, vor dem Verständnis dessen, was das System tut, zu optimieren – oder gar zu vergessen, überhaupt zu optimieren.
- Der größte Schaden für die Wartbarkeit entsteht durch angesammelte Komplexität: übermäßiger Client-Code, mehrfache Caches, vermeidbare Hydratierungsprozesse sowie spekulative Abstraktionen.
- Das Weglassen von Funktionalitäten ist in der Regel besser als das Hinzufügen neuer Mechanismen – egal ob dadurch weniger JavaScript ausgeliefert wird, Komponenten auf dem Server verbleiben, der Zustand vereinfacht wird, überflüssige Anfragen gestrichen werden, eine langsame Backend-Anfrage behoben wird oder eine Optimierung entfernt wird, die mehr kostet, als sie einspart.
- Einige Anwendungen benötigen tatsächlich ausgefeilte Caching- oder Rendering-Strategien, doch diese Entscheidung sollte auf Messungen und einer klaren Diagnose beruhen.
- Die schnellste Anwendung ist oft diejenige, die am wenigsten unnötige Arbeit leistet.