Zum Inhalt springen

Das Rendering-Modell des Next.js App Routers, richtig erklärt

Statisch, dynamisch, gestreamt und revalidiert sind keine vier Einstellungen, sondern ein Modell mit wenigen Schaltern. Zu wissen, welchen man umgelegt hat, ist der Großteil der Performance-Arbeit.

4 Min. Lesezeit

Fast jedes Next.js-Performance-Problem, zu dem wir gerufen werden, lässt sich auf denselben Satz reduzieren: niemand wusste, dass diese Route dynamisch geworden war.

Nicht, dass dynamisches Rendering falsch wäre. Das Problem ist, dass der Wechsel von statisch zu dynamisch implizit passiert. Er passiert, weil jemand vier Komponenten tief ein Cookie gelesen hat, und nichts im Pull Request hat das erwähnt.

Dieser Artikel ist das mentale Modell, das wir jedem Team beibringen, mit dem wir arbeiten.

Es gibt keine vier Rendering-Modi

Es gibt ein Modell — den Komponentenbaum auf dem Server rendern — und die einzige echte Frage lautet, wann dieses Rendering passiert und wie viel davon wiederverwendet wird.

Wann gerendert wirdWie es üblicherweise heißt
Zur Build-Zeit, dauerhaft wiederverwendetStatisch
Zur Build-Zeit, ersetzt auf ein Signal hinIncremental Static Regeneration
Bei jeder AnfrageDynamisch
Build-Zeit für die Hülle, Anfragezeit für die LöcherPartial Prerendering

Streaming steht quer zu alldem. Beim Streaming geht es um die Reihenfolge der Auslieferung, nicht darum, wann die Arbeit passiert.

Was eine Route tatsächlich dynamisch macht

Eine Route ist statisch, bis etwas darin die Anfrage braucht. Jeder dieser Zugriffe nimmt die Route aus dem statischen Rendering heraus:

import { cookies, headers } from 'next/headers';
import { connection } from 'next/server';
 
await cookies();       // needs the request
await headers();       // needs the request
await connection();    // explicitly asks for the request

searchParams in den Props einer Page tut dasselbe. Ebenso fetch mit cache: 'no-store', und ebenso jeder unstable_noStore-Aufruf, der von einem früheren Refactoring übrig geblieben ist.

Entscheidend ist: das wirkt nach oben ansteckend, nicht nach unten. Eine Server Component tief im Baum, die cookies() liest, macht die gesamte Route dynamisch, denn die Route kann nicht vorgerendert werden, wenn irgendein Teil davon eine Anfrage braucht, die es noch gar nicht gibt.

Deshalb lautet die Audit-Frage nie „ist diese Seite statisch?". Sie lautet: „was ist das Tiefste in diesem Baum, das die Anfrage berührt, und muss es das?"

Der häufige Unfall

// app/products/[slug]/page.tsx
export default async function Page({ params }) {
  const { slug } = await params;
  const product = await getProduct(slug);
 
  return (
    <>
      <ProductDetail product={product} />
      <RecentlyViewed />   {/* reads cookies() — the whole page is now dynamic */}
    </>
  );
}

Die Produktdetails hätten ein Jahr lang statisch sein können. Ein personalisierter Streifen in der Ecke sorgt dafür, dass jede Anfrage das Ganze neu rendert.

Die Lösung ist nicht, den Streifen zu löschen. Sie ist, ihn hinter eine Suspense-Grenze zu schieben, damit der Rest der Seite vorgerendert werden kann:

<Suspense fallback={<RecentlyViewedSkeleton />}>
  <RecentlyViewed />
</Suspense>

Mit aktiviertem Partial Prerendering wird die statische Hülle sofort vom Edge ausgeliefert und nur das Loch pro Anfrage berechnet. Ohne es bekommen Sie immerhin die Hülle zuerst gestreamt, statt dass die ganze Seite wartet.

Revalidierung ist ein Signal, kein Timer

Die meisten Teams greifen zu einem zeitbasierten revalidate, weil es die erste Option in der Dokumentation ist:

export const revalidate = 3600;

Das ist eine Schätzung darüber, wie oft sich Ihre Inhalte ändern. Tag-basierte Revalidierung ist eine Tatsache darüber, wann sie sich geändert haben:

// Reading side
const posts = await fetch(`${API}/posts`, {
  next: { tags: ['posts'] },
});
 
// Writing side — your CMS webhook route
import { revalidateTag } from 'next/cache';
 
export async function POST(request: Request) {
  await verifyWebhookSignature(request);
  revalidateTag('posts');
  return Response.json({ revalidated: true });
}

Der Unterschied in der Praxis: Mit einem Ein-Stunden-Timer veröffentlicht Ihre Redaktion eine Korrektur und drückt dann fünfzig Minuten lang auf Neu laden. Mit einem Tag ist die Korrektur in Sekunden live, und die Seite wird ansonsten unbegrenzt aus dem Cache ausgeliefert. Frischer und günstiger.

Request-Memoisierung ist nicht der Data Cache

Die beiden werden ständig vermengt, und sie lösen verschiedene Probleme.

Request-Memoisierung entdoppelt identische fetch-Aufrufe innerhalb eines einzigen Render-Durchlaufs. Sie existiert, damit Sie getUser() im Layout und noch einmal in der Page aufrufen können, ohne zwei Roundtrips zu bezahlen. Sie gilt pro Rendering und verschwindet, wenn das Rendering endet.

Der Data Cache überdauert Anfragen und Deployments. Auf ihn wirken revalidate und revalidateTag.

Wenn Sie eine Datenquelle ohne fetch kapseln — etwa einen Datenbank-Client — bekommen Sie keine Memoisierung geschenkt. Nutzen Sie Reacts cache:

import { cache } from 'react';
import 'server-only';
 
export const getUser = cache(async (id: string) => {
  return db.user.findUnique({ where: { id } });
});

Ohne diesen Wrapper setzen ein Layout und drei Komponenten, die jeweils den aktuellen Nutzer brauchen, vier identische Queries pro Rendering ab. Genau das haben wir in Produktion öfter gefunden, als uns lieb ist.

Eine Route in fünf Minuten auditieren

  1. next build ausführen und die Routentabelle lesen. ist statisch, ist SSG mit Parametern, ƒ ist dynamisch. Alles unerwartet ƒ ist Ihre Liste.
  2. Für jede Überraschung den Baum nach cookies, headers, searchParams, no-store und connection durchsuchen.
  3. Fragen, ob diese Daten für den ersten Bildaufbau gebraucht werden oder nur für ein personalisiertes Fragment. Fragmente kommen hinter Suspense.
  4. Prüfen, dass jede Datenfunktion ohne fetch in cache gewickelt ist.
  5. Zeitbasiertes revalidate überall dort durch Tags ersetzen, wo ein Schreibpfad existiert.

Diese Liste ist die erste Stunde der meisten Performance-Projekte, die wir durchführen, und dort versteckt sich normalerweise der größte einzelne Gewinn.

Was Ihnen das bringt

Eine statisch gerenderte Route liefert ihr HTML aus einem CDN-Edge-Knoten in einstelligen Millisekunden. Dieselbe Route dynamisch gerendert lässt bei jeder Anfrage Ihren Komponentenbaum, Ihre Datenschicht und Ihre Datenbank laufen. Der Unterschied ist kein Prozentsatz. Er ist eine Größenordnung, und er zeigt sich im LCP, in den Infrastrukturkosten und darin, was mit Ihrer Site passiert, wenn ein Link an einem Dienstagnachmittag viral geht.

Zurück zu allen Artikeln