Zum Inhalt springen

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.

6 Min. Lesezeit

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 build

Sie 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. Intl funktioniert auf dem Server. Geht es um die Zeitzone der Besucherin, reichen Sie die formatierte Zeichenkette nach unten durch oder rendern Sie ein <time>-Element mit dateTime-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> mit strategy="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.

Die Konvention funktioniert nur, wenn alle dasselbe Modell davon haben, was eine Server-Komponente ist, also lesen Sie das Rendering-Modell einmal richtig. Die Hälfte der Grenzen, die wir entfernen, entpuppt sich als Formular, und dort beseitigt eine Server Action die Client-Komponente, statt sie zu verkleinern. Ist das Bundle bereits groß und lautet die Frage, welche Grenzen zuerst verschoben werden, messen wir das.

Verwandte Fragen

Heißt use client, dass die Komponente nur im Browser läuft?
Nein. Es markiert, wo das Client-Bundle beginnt. Alles, was darunter importiert wird, geht an den Browser, und die Komponente selbst rendert für das erste HTML weiterhin auf dem Server.
Wie finden wir, wohin die Grenze gewandert ist?
Mit dem Bundle-Analyzer, gelesen als Importgraph statt als Treemap. Gesucht ist die eine Client-Komponente weit oben in einem Baum, die alles darunter mit hinübergezogen hat.
Kann eine Server-Komponente in einer Client-Komponente stehen?
Ja, als children oder als Prop übergeben. Das ist das Muster, das eine Grenze flach hält, und das, zu dem die meisten Teams nicht greifen.
Ist ein Context-Provider an der Wurzel ein Problem?
Er ist die häufigste Ursache für eine zu hoch sitzende Grenze. Umschließen Sie den kleinsten Teilbaum, der den Context braucht, und wenn das wirklich die ganze Anwendung ist, halten Sie den Provider dünn, damit wenig hinübergeht.

← Zurück zu allen Artikeln

Anrufen+1 848 272 7583WhatsApp+90 850 308 5436E-Mailinfo@codefacture.comKontaktseite