Was `use client` Sie wirklich kostet
Die Client-Grenze ist keine Performance-Einstellung, sie ist eine Bundle-Grenze. So finden Sie heraus, wohin Ihre nach oben gewandert ist — und was es bringt, sie zurückzuschieben.
use client bedeutet nicht „diese Komponente läuft im Client". Jede Komponente
einer Next.js-Anwendung wird mindestens einmal auf dem Server gerendert.
Was use client tatsächlich erklärt, ist eine Grenze: von diesem Modul
abwärts wird alles in das JavaScript-Bundle kompiliert und an den Browser
ausgeliefert, damit es hydratisieren kann. Das ist der Preis. Nicht das
Rendern — das Ausliefern.
Sobald man diese Definition im Kopf hat, werden die üblichen Fehler offensichtlich.
Das Abdriften nach oben
Grenzen wandern zur Wurzel, eine vernünftige Entscheidung nach der anderen.
Das Design wünscht sich ein Dropdown im Header. Der Header braucht useState,
also kommt use client an den Anfang von Header.tsx. Der Header importiert
Nav, das NavItem importiert, das eine formatDate-Hilfsfunktion
importiert, die eine Datumsbibliothek importiert. Nichts davon musste
interaktiv sein. Alles davon liegt jetzt im Bundle, auf jeder Seite, für jeden
Besucher.
Sechs Monate später ist das Layout eine Client-Komponente und niemand weiß mehr, warum.
Die eigene Grenze finden
Die Build-Ausgabe verrät, wo es schiefging. Sehen Sie sich First Load JS pro Route an:
Route (app) Size First Load JS
┌ ○ / 1.2 kB 184 kB
├ ○ /about 0.8 kB 184 kB
└ ○ /pricing 2.1 kB 186 kB
Wenn jede Route dieselbe große Grundlast trägt, liegt das Gewicht im gemeinsamen Layout, nicht in den Seiten. Das ist die Signatur einer Client-Grenze, die in den Layout-Baum geklettert ist.
Für die Details lassen Sie den Bundle-Analyzer laufen:
npm install --save-dev @next/bundle-analyzer
ANALYZE=true npm run buildSie suchen zwei Dinge: Bibliotheken, die Sie nicht erwartet haben, und Bibliotheken, die im gemeinsamen Chunk auftauchen, obwohl sie im Chunk einer einzelnen Route gehörten.
Die Grenze nach unten schieben
Die Lösung hat fast immer dieselbe Form — das interaktive Blatt bleibt eine Client-Komponente, alles darum herum bleibt auf dem Server.
Statt dessen:
'use client';
import { heavyMarkdownRenderer } from 'some-large-lib';
export function Article({ content, comments }) {
const [showComments, setShowComments] = useState(false);
return (
<article>
{heavyMarkdownRenderer(content)}
<button onClick={() => setShowComments(true)}>Comments</button>
{showComments ? <Comments data={comments} /> : null}
</article>
);
}so:
// Article.tsx — server component, no 'use client'
import { heavyMarkdownRenderer } from 'some-large-lib';
import { CommentsToggle } from './CommentsToggle';
export function Article({ content, comments }) {
return (
<article>
{heavyMarkdownRenderer(content)}
<CommentsToggle>
<Comments data={comments} />
</CommentsToggle>
</article>
);
}// CommentsToggle.tsx — the only client component
'use client';
export function CommentsToggle({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Comments</button>
{open ? children : null}
</>
);
}Die Markdown-Bibliothek bleibt auf dem Server. Comments bleibt eine
Server-Komponente, obwohl sie innerhalb einer Client-Komponente gerendert wird
— weil sie als children übergeben wird, war sie bereits auf dem Server
gerendert und kommt als serialisierte Ausgabe an, nicht als Code.
Children, die durch eine Client-Komponente gereicht werden, überqueren die Grenze nicht. Diese eine Regel löst die meisten Fälle auf, in denen Teams glauben, sie müssten zwingend auf die Client-Seite wechseln.
Warum das im INP auftaucht, nicht nur in der Ladezeit
Interaction to Next Paint misst, wie lange der Haupt-Thread braucht, um zu reagieren, wenn eine Besucherin etwas antippt. Jedes Kilobyte JavaScript im Bundle wird geparst, kompiliert und ausgeführt, bevor die Seite auf irgendetwas reagieren kann, und die Hydratation konkurriert mit dem ersten Tippen.
Auf einem Android-Gerät der Mittelklasse — also dem Gerät des größten Teils der Welt — sind 300 KB JavaScript grob eine Sekunde Arbeit im Haupt-Thread, bevor die Hydratation fertig ist. Ein Tippen in dieser Sekunde landet in der Warteschlange. Das ist ein verfehltes INP, und keine noch so gute CSS-Arbeit repariert es.
Server Components sind in erster Linie keine Rendering-Optimierung. Sie sind eine Art, den Code gar nicht erst zu senden.
Was keine Client-Komponente sein muss
Aus den Audits, die wir durchführen, die wiederkehrenden Fehlalarme:
- Datums- und Zahlenformatierung.
Intlfunktioniert auf dem Server. Geht es um die Zeitzone der Besucherin, reichen Sie die formatierte Zeichenkette nach unten durch oder rendern Sie ein<time>-Element mitdateTime-Attribut und überlassen die Anzeige CSS oder einer winzigen Blattkomponente. - Ein Theme lesen. Das
data-theme-Attribut plus CSS erledigt das ohne JavaScript. - Daten beim Mounten laden. Sind sie nicht nutzerspezifisch, laden Sie sie auf dem Server. Sind sie es, laden Sie sie auf dem Server hinter Suspense.
- Analytics. Laden Sie sie in einem
<Script>mitstrategy="afterInteractive"oder"lazyOnload", nicht in einer Komponente. - Icon-Bibliotheken. Icons sind Markup. Importieren Sie das einzelne SVG, nicht die Barrel-Datei, die zweitausend davon hereinzieht.
Die Review-Regel, die wir anwenden
In jeder Codebasis, in der wir arbeiten, wird use client behandelt wie ein
dangerously-Präfix: erlaubt, manchmal richtig, und es besteht nie ein
Review ohne einen Satz, der erklärt, warum die Grenze genau in diese Datei
gehört und nicht eine Ebene tiefer.
Diese eine Konvention ist mehr wert als jedes Bundle-Size-Werkzeug, weil sie das Problem in dem Moment abfängt, in dem es entsteht, statt achtzehn Monate später, wenn endlich jemand den Analyzer öffnet.
