adesso Blog

Wenn es um die Wahl eines Frameworks für ein neues React-Projekt geht, ist Next.js die erste Wahl - und das schon seit mehreren Jahren. Im Vergleich zu seinen Konkurrenten bietet Next.js eine hohe Performance bei Webmetriken, da es sich auf Server-Side Rendering (SSR) konzentriert. Der ursprüngliche Hauptkonkurrent Gatsby konzentriert sich stattdessen auf Static Site Generation (SSG). Der neue Konkurrent Remix bietet ähnliche SSR-Vorteile wie Next.js, hat aber in Bezug auf die Akzeptanz in der Industrie noch Nachholbedarf.

Die Notwendigkeit von Server-Side Rendering

Aber wozu braucht man überhaupt ein Framework? Was React von Haus aus gut kann, sind Client-Side Rendered (CSR) Single-Page Applications (SPA). Diese bieten ein großartiges Weberlebnis, da die User sofort zwischen den Seiten navigieren können, ohne darauf warten zu müssen, dass diese vom Server geladen werden.

Der große Nachteil von SPA ist, dass die erste Seite, die die User besuchen, in einem JavaScript-Bündel an ihren Browser gesendet wird, bevor sie als HTML dargestellt wird. Dies wirkt sich negativ auf wichtige Webmetriken wie First Contentful Paint (FCP) und Total Blocking Time (TBT) aus.

Beide Metriken (FCP und TBT) profitieren stark von SSR und SSG. SSR wird auf dem Server gestartet, sobald die erste Anfrage eintrifft. Er rendert Ihre React-Seiten auf dem Server in HTML, bevor sie an den Browser ausgeliefert werden. Dies ist besonders vorteilhaft für dynamische öffentliche Seiten (zum Beispiel Wetterinformationen), bei denen die HTML-Ausgabe zwischengespeichert und an verschiedene Benutzerinnen und Benutzer ausgeliefert werden kann, was bei nachfolgenden Anfragen Zeit spart. SSG rendert statische Seiten, bevor Ihre Website ausgeliefert wird, wodurch die Renderingzeit vollständig aus dem Request-Response-Lebenszyklus entfernt wird. Dies kann im Vergleich zu SSR zu einer besseren Time to First Byte führen.

Es ist zu beachten, dass statisch generierte Seiten dennoch dynamische Inhalte und Interaktionen bieten können, indem sie nach dem Laden des ursprünglichen HTML-Signals JavaScript-Anfragen an den Browser senden. Bei interaktiven dynamischen Seiten kann diese Kombination im Vergleich zu SSR zu einer schlechteren First Input Delay führen.

Der große Wettbewerbsvorteil von Next.js liegt in der Fähigkeit, einen leistungsfähigen hybriden SSR/SSG-Ansatz mit einer sehr intuitiven Schnittstelle anzubieten. Es vereinfacht das Routing, indem es den React-Router in ein dateibasiertes Routing-System einbettet, während der Rest der React-Schnittstelle intakt bleibt und der Entwicklerin oder dem Entwickler volle Flexibilität bietet.

Die Kompromisse

Next.js ist derzeit der klare Gewinner in Bezug auf die Popularität am Markt (gemessen an der StateOfJS-Metrik "usage").

Bindung an den Anbieter

Next.js ist heftig kritisiert worden, weil es schwer ist, es anderswo als bei Vercel zu verwenden, einer Next.js-Hosting-Lösung, die Vercel (dem Unternehmen) gehört, dem Betreiber von Next.js. Zwar kann Next.js derzeit so konfiguriert werden, dass es mit anderen Hosting-Plattformen (insbesondere Netlify und AWS) zusammenarbeitet, aber um mit den neuesten Funktionen auf dem Laufenden zu bleiben, muss man möglicherweise mit Vercel arbeiten und ist damit der Preisgestaltung des Unternehmens ausgeliefert.

React-Server-Komponenten

Im Mai 2023 veröffentlicht Next.js die Version 13.4, die den neuen App-Router als bevorzugtes dateibasiertes Routing-System unterstützt. Diese Version führt die experimentelle Syntax "use client" ein, um React Server Components zu unterstützen. Da diese Funktionen derzeit von React als experimentell eingestuft werden, bieten einige Bibliotheken im React-Ökosystem (zum Zeitpunkt der Erstellung dieses Artikels) noch keine vollständige Unterstützung dafür. Ein Beispiel hierfür ist die React-Test-Library, die derzeit keine asynchronen Server Components darstellen kann.

Der App-Router

Während sich der App-Router aufgrund von Namensänderungen im dateibasierten Routing-System grundlegend vom Page-Router unterscheidet, liegt der Hauptunterschied, den wir betrachten wollen, in der Server-Komponente.

Server- vs. Client-Komponenten

Im App-Router wird jede React-Komponente standardmäßig als Server-Komponente behandelt, es sei denn, sie wird explizit als Client-Komponente über 'use client' markiert. Einige Komponenten, wie page routes, müssen Server-Komponenten sein, während andere entweder Server- oder Client-Komponenten sein können.

Bevor wir ins Detail gehen, ist es wichtig zu verstehen, wo die Grenze zwischen Server- und Client-Komponenten verläuft. Es sei daran erinnert, dass React, wenn es standardmäßig verwendet wird, HTML auf dem Client mit CSR rendert. SSR-Frameworks wie Next.js führen den gleichen Rendering-Schritt stattdessen auf dem Server aus. Diese SSR-Funktionalität gilt sowohl für Server- als auch für Client-Komponenten. Der Unterschied besteht darin, dass Client-Komponenten auf dem Client (teilweise) hydratisiert werden, während Server-Komponenten überhaupt keine JS an den Client senden. Die folgende Tabelle zeigt einige der wichtigsten Unterschiede zwischen Server- und Client-Komponenten:

Bibliotheksunterstützung

Bestehende, ausgereifte Bibliotheken im React-Ökosystem wurden oft mit Blick auf Client-Komponenten entwickelt und bieten daher keine grundlegende Unterstützung für Server-Komponenten.

Client-seitige Funktionalität

Der Versuch, Bibliotheken mit reiner Client-Logik in Server-Komponenten zu verwenden (alle benutzerdefinierten Komponenten im App-Router werden standardmäßig als Server-Komponenten betrachtet), führt zu schwer zu debuggenden Fehlern auf dem Next.js-Server. Hier ist ein Beispiel aus dem Quickstart Guide von react-hook-form:

Unsere Formular-Komponente sieht folgendermaßen aus:

	
	import { useForm } from "react-hook-form";
		export default function Home() {
		  const {
		    register,
		    handleSubmit,
		    formState: { errors },
		  } = useForm();
		  return (
		    <form onSubmit={handleSubmit((data) => console.log(data))}>
		    ...
	

Der Versuch, diese Komponente zu rendern, führt zu dem folgenden runtime error:

	
	 ⨯ src/app/page.tsx (8:13) @ useForm
		 ⨯ TypeError: (0 , react_hook_form__WEBPACK_IMPORTED_MODULE_1__.useForm) is not a function
		    at Home (./src/app/page.tsx:11:119)
		    at stringify (<anonymous>)
		digest: "503401641"
	

Das Hinzufügen einer 'use client'-Anweisung zu dieser Komponentendatei macht sie zu einer Client-Komponente und löst das Problem. Wir können so tun, als ob react-hook-form die Server-Komponenten-Architektur unterstützt, indem wir einen Wrapper dafür erstellen:

	
	// src/app/react-hook-form.ts
		"use client";
		export * from "react-hook-form";
		// src/app/page.tsx
		import { useForm } from "./react-hook-form";
		// ...
	

Dadurch wird das Problem zwar nicht behoben, aber wir haben jetzt eine viel bessere Fehlermeldung zur Verfügung:

	
	Error: Attempted to call useForm() from the server but useForm is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.
		    at Home (./src/app/page.tsx:12:120)
		    at stringify (<anonymous>)
		digest: "1355241539"
	

Dieses Verhalten würde bedeuten, dass alle Bibliotheksverwalter explizit "use client" zu ihren Client-Komponenten und Hooks hinzufügen müssten, nur um die Next.js-Architektur zu unterstützen. Sieben Monate nach der stabilen Veröffentlichung des App-Routers hinken populäre Bibliotheken wie react-hook-form immer noch hinterher, was vielleicht ein Zeichen dafür ist, dass die Open-Source-Community von React die Server-Komponenten-Architektur des App-Routers nicht unterstützt.

Testen

Die Render-Funktion der react-test-library (RTL) kann derzeit keine asynchronen Serverkomponenten rendern. Bestehende Workarounds ermöglichen das Rendern solcher Komponenten isoliert durch Verlassen der JSX-Syntax, aber diese grundlegenden Workarounds können keine asynchronen Komponenten verarbeiten, die in einem Komponentenbaum verschachtelt sind. Der Verzicht auf die JSX-Syntax erschwert auch die Verwendung der in RTL integrierten Funktionen ( zum Beispiel Wrapper).

Die Server-Client-Grenze

Server-Komponenten können nur serialisierbare Daten an Client-Komponenten weitergeben. Dies erschwert die Anwendung von Patterns wie Dependency Injection, da Funktionen nicht über die Server-Client-Grenze hinweg übergeben werden können.

Wrapper-Komponenten

Ein frühes Muster, das sich in unserer eigenen Codebasis entwickelt hat, ist die Verwendung von Wrapper-Komponenten über benutzerdefinierte Hooks. In traditionellen Client-first React Anwendungen war der Kompromiss zwischen diesen beiden Optionen relativ ausgewogen. Hier sind einige Vor- und Nachteile der beiden Optionen:

1. Wrapper Components:
  • Das Erzwingen einer expliziten Kopplung zwischen Datenabruf und Datenbereitstellung führt dazu, dass die Komponenten aufgrund von Überschneidungen schwieriger zu benennen sind:
	
	 	import { ProductTeasers } from "./product-teasers";
		function ProductTeasersWrapper() {
		  const products = getProducts();
		  return <ProductTeasers products={products} />;
		}
	
2. Custom Hooks:
  • Herstellung einer impliziten Verbindung zwischen dem Abrufen von Daten und dem Rendern von Daten:
	
	 	// src/app/use-products.js
		function useProducts() {
		  const products = getProducts();
		  return products;
		}
		 	// src/app/products/page.jsx
		function ProductsPage() {
		  // Do we use this hook in the consumer...?
		  const products = useProducts();
		  return <ProductTeasers products={products} />;
		}
		 	// src/app/products/teasers.jsx
		function ProductTeasers() {
		  // ...or inside the component itself?
		  const products = useProducts();
		  return products.map((product) => <ProductTeaser product={product} />);
		}
	
  • Extrahieren der Logik ohne Überladen des Komponentenbaums.

Im App-Router wird das Wrapper Component Pattern sehr intuitiv, denn wir wollen folgendes machen:

  • 1. Datenabruf auf dem Server
  • 2. Rendering auf dem Client

Die Namensgebung ist hier noch ungeschickt, aber wir können eine explizite Kopplung erzeugen, indem wir bei Bedarf "use client" bereitstellen. Am Ende sieht es ungefähr so aus:

	
	// src/app/products/teasers.tsx
		// This is our server-side wrapper component.
		// It gets a concise name because it's what we want to expose to its consumers.
		async function ProductTeasers() {
		  const products = await fetchProducts();
		  return <ProductTeasersForClient products={products} />;
		}
		// src/app/products/teasers.client.jsx
		"use client";
		// Our inner client component gets a longer name to avoid overlap
		function ProductTeasersForClient({ products }) {
		  return products.map((product) => <ProductTeaser product={product} />);
		}
	

Das Schreiben von benutzerdefinierten Hooks im App-Router erscheint umständlich, da sie einfach als normale Funktionen enden (also keine React-Hooks verwenden), wenn sie für Serverkomponenten geschrieben werden. Wir haben festgestellt, dass Wrapper-Komponenten ein besseres Modell für unsere aktuellen Anwendungsfälle sind.

Fazit

React bietet natives Client-Side Rendering (CSR). Server-Side Rendering (SSR) und Static Site Generation (SSG) Frameworks wie Next.js sind unerlässlich, wenn ihr möchtet, dass Ihre React-Anwendung bei Webmetriken gut abschneidet. Next.js ist in die Kritik geraten, weil es auf anderen Plattformen als Vercel schwer zu implementieren ist. Die verfrühte Einführung der React Server Components hat dazu geführt, dass einige der ausgereiftesten Bibliotheken von React nicht unterstützt werden.Die Architektur des App Routers bringt einige grundlegende Änderungen mit sich, die es der Open-Source-Gemeinschaft erschweren, mit der Tooling-Unterstützung Schritt zu halten.

Abschließende Worte

Die Wahl zwischen den Next.js-Routern ist derzeit sehr schwierig. Neue Anwendungen mit dem alten Page Router zu bauen erscheint fragwürdig, da die Transformation bereits weit fortgeschritten ist. In der Zwischenzeit scheint der neue App Router noch nicht bereit zu sein, seinen Platz als bevorzugte Methode zum Schreiben moderner React Anwendungen einzunehmen. Ist das der Anfang vom Ende von Next.js? Oder ebnet es den Weg für eine neue Ära des serverseitigen Renderings in React? ƒNur die Zeit wird es zeigen.

Ihr möchtet mehr über spannende Themen aus der adesso-Welt erfahren? Dann werft auch einen Blick in unsere bisher erschienenen Blog-Beiträge.

Bild Bjarki Sigurðsson

Autor Bjarki Sigurðsson

Bjarki Sigurðsson arbeitet seit über fünf Jahren als Full-Stack-Entwickler und ist seit September 2023 als Senior Software Engineer bei adesso tätig. Er hat sich auf React-Anwendungen spezialisiert und Next.js ist seit 2019 das Framework seiner Wahl.

Diese Seite speichern. Diese Seite entfernen.