Blog

Reacts neuster Streich: Server Components

Aug 4, 2021

Hans-Christian Otto

Das Jahr 2020 endete für React-Entwickler mit einem lauten Knall, als das React-Team von Facebook kurz vor Weihnachten den RFC für React Server Components vorstellte. Und auch wenn Facebook selbst davon spricht, dass React Server Components noch Zukunftsmusik sind, wollen wir uns in diesem Artikel einmal mit dem Thema auseinandersetzen. Seit der Vorstellung von Hooks könnten React Server Components die größte Neuerung für React-Entwickler werden.

Um die Frage zu beantworten, warum man eigentlich React Server Components benötigt, ist es sinnvoll, ein wenig auszuholen. Ich selbst entwickle bereits seit über 10 Jahren sogenannte Single Page Applications (SPAs). Das sind Anwendungen, bei denen der Benutzer nur einmal eine HTML-Seite aufruft und die weitere Navigation per JavaScript und nachgeladenen Daten stattfindet. Damals hieß das Ganze zwar noch nicht SPA, sondern RIA (Rich Internet Application), die Grundidee ist aber dieselbe geblieben. Diese Anwendungen haben in den letzten 10 Jahren an Popularität gewonnen. Oft wird bei neuen Webanwendungen direkt zu Angular oder React gegriffen, die ja auch viele Vorteile besitzen. Aber wie immer im Leben gehen diese Vorteile einher mit Nachteilen.

Nachteile von SPAs

Einer der Nachteile von Single Page Applications ist, dass sie oft nicht gut von Suchmaschinen indiziert werden können, da sie eben nur ein kleines HTML-Grundgerüst ohne Inhalte ausliefern und der Rest per JavaScript nachgeladen wird. Suchmaschinen wie Google arbeiten zwar daran, auch solche Seiten zu indizieren, darauf verlassen sollte man sich aber trotzdem nicht. Darüber hinaus neigen SPAs dazu, riesige Mengen an JavaScript zu laden, bevor der User anfangen kann, die Anwendung zu verwenden. Das ist zwar auch ein Problem, das sich aufgrund von immer schnelleren Internetzugängen reduziert, aber spätestens, wenn man im ICE zwischen Köln und Düsseldorf sitzt, ärgert man sich doch wieder darüber, dass sich eine ganze Zeit lang nichts tut. Aus der Kombination der beiden Probleme ergibt sich dann, dass ein User, der nun eine Minute darauf wartet, dass „die paar Megabyte JavaScript“ heruntergeladen werden, die ganze Zeit nur auf eine weiße Seite blickt. Und das will natürlich niemand den eigenen Usern zumuten. Da auch Google erkannt hat, dass User ungerne warten, fließt die Ladezeit mittlerweile auch in die Suchmaschinenplatzierung ein.

Server-side Rendering als Lösungsansatz?

Als ein Lösungsansatz für das Problem, dass Suchmaschinen SPAs nicht indizieren können und User während der Ladezeit keine weiße Seite sehen sollen, hat sich Server-side Rendering, kurz SSR, etabliert. Insbesondere in der React-Community redet man bereits seit vielen Jahren davon, dass Komponenten und ganze Anwendungen „isomorph“ seien sollen. Das ist ein Fachbegriff aus der Mathematik, der aber in diesem Kontext eigentlich nur heißt: Die Komponente bzw. Anwendung kann sowohl serverseitig als auch im Browser ausgeführt werden. Üblicherweise rendert der Server mit Hilfe von Node.js die Komponenten für die Darstellung im Browser. Das daraus resultierende HTML-Grundgerüst wird dann an den Client gesendet. Das Rendering dort sollte abgeschlossen sein, bevor das Laden des JavaScript-Codes beginnt und die SPA interaktiv wird.

Wir können also mit Server-side Rendering dafür sorgen, dass die weiße Seite nicht mehr so lange zu sehen ist und Suchmaschinen unsere Seite indizieren können. Trotzdem müssen wir den kompletten JavaScript-Code herunterladen und genaugenommen wird das Rendering, das serverseitig durchgeführt wurde, im Client noch einmal durchgeführt.

React Server Components vorgestellt

Genau hier können React Server Components ihre Stärken ausspielen. React Server Components erlauben uns, einzelne Komponenten mitsamt ihren Abhängigkeiten komplett auf den Server zu verlagern. Das bedeutet, dass sie im Browser nicht mehr ausgeführt werden, und das bedeutet, dass die Menge an JavaScript, die heruntergeladen (und ausgeführt) werden muss, schrumpft. In ersten Experimenten von Facebook in einer Produktionsumgebung führten React Server Components zu einer Einsparung von 29 Prozent bei der Bundle-Größe, also von fast einem Drittel, wie Facebook in einem Videovortrag zu React Server Components berichtet [1]. Und das ist sicher erst der Anfang.

Schauen wir uns einmal ein Beispiel an, wie das Ganze aussehen kann. Dafür gehen wir davon aus, dass wir uns in einem Projekt bewegen, das bereits React Server Components unterstützt. Facebook stellt hierfür ein Demoprojekt bereit. Wir beginnen mal mit einer Komponente, die einen Markdown-Text als HTML darstellt. Dafür verwenden wir in Listing 1 die Open-Source-Bibliothek marked.

Listing 1

import marked from 'marked';
 
const longMarkdownString = `/* Langer Text */`;
export default function MarkdownExample() {
  return (
    <div
      className="markdown"
      dangerouslySetInnerHTML={{
        __html: marked(longMarkdownString),
      }}
    />
  );
}

Die Syntax mit dangerouslySetInnerHTML ist vielleicht etwas ungewohnt, hat aber nichts mit React Server Components zu tun, sondern sorgt einfach dafür, dass wir HTML ohne irgendeine Form von Sanitation direkt rendern. Ansonsten sieht die Komponente aber wie eine ganz normale React-Komponente aus, und das ist sie auch. Sie lässt sich problemlos in eine bestehende React-Anwendung einbinden und wird dann im Browser ausgeführt – inklusive unserer Abhängigkeit zu marked. Wir können nun aus der Komponente eine React Server Component machen. Das machen wir, indem wir die Datei von MarkdownExample.js in MarkdownExample.server.js umbenennen. Und schon wird die Datei nicht mehr im Browser ausgeführt, sondern auf unserem Server. Das hat zur Folge, dass die Markdown-Bibliothek nicht mehr im Browser benötigt wird – stattdessen wird die Konvertierung von Markdown nach HTML serverseitig durchgeführt. Im Beispielcode haben wir den Markdown-String einfach in einer Variablen abgelegt. Stattdessen könnten wir hier serverseitig aber auch mit einem API kommunizieren. Das muss dabei nicht von unserem Browser aus erreichbar sein, sondern nur von unserem Server. Wir könnten die Daten auch einfach aus dem Dateisystem des Servers auslesen, oder gar aus einer serverseitigen Datenbank.

Man kann sich an dieser Stelle natürlich fragen, wo der Vorteil gegenüber einem REST API ist, das unser Markdown direkt als HTML bereitstellt. Auch wenn die Frage berechtigt ist, werden wir sehen, dass es durchaus noch Vorteile von React Server Components gegenüber der Verwendung eines solchen APIs gibt.

Im Kontext von React Server Components, die mit dem Dateinamensuffix .server.js kenntlich gemacht werden, gibt es auch noch die Client Components, die wenig verwunderlich .client.js als Suffix im Dateinamen tragen. Alle Komponenten, die wir bisher für den Browser entwickelt haben, sind erstmal auch solche Clientkomponenten. Es gibt aber auch die Mischung aus beiden, sogenannte Shared Components, die sowohl auf dem Client als auch dem Server laufen können.

Für ein Beispiel stellen wir uns vor, dass wir ein CMS entwickeln, das Markdown-Inhalte darstellt. Für die Endbenutzer rendern wir das Markdown in einer React Server Component und senden es fertig gerendert an den User. Ein Redakteur könnte zusätzlich neben dem Inhalt einen Bearbeiten-Button angezeigt bekommen. Wenn dieser Button geklickt und dem Redakteur ein Textfeld zum Editieren angeboten wird, können wir unsere Markdown-Komponente auch wieder clientseitig verwenden und dem Redakteur eine Vorschau seiner Inhalte in Echtzeit anzeigen – ohne mit dem Server kommunizieren zu müssen. Die Markdown-Komponente mit ihrer Abhängigkeit auf die marked-Bibliothek müssen wir dann aber erst in dem Moment laden, in dem der Redakteur auf Bearbeiten klickt, und nicht schon, wenn der Endbenutzer sich den Artikel ansieht. Dafür brauchen wir natürlich einen Bundler, der das unterstützt. Das React-Team bei Facebook kooperiert hierfür bereits mit dem webpack-Team und es gibt bereits eine Vorabversion eines Plug-ins, dass dieses Problem für uns löst.

Betrachten wir also nochmal unsere MarkdownExample-Komponente. Diese kann nun also sowohl als React Server Component als auch als Client Component verwendet werden. Da sich React Server Components und SSR nicht ausschließen, können wir diese Komponente auch problemlos über Server-side Rendering mit z. B. NextJS oder Frontastic verwenden.

Man kann aber natürlich weiterhin die Frage stellen, ob nicht ein einfaches API für die Konvertierung von Markdown zu HTML ebenso gut funktionieren würde. Und natürlich ist das erstmal so. Für den Redakteur müssten wir die Funktionalität dann aber nochmal in JavaScript nachbauen oder die Vorschau wäre nicht mehr in Echtzeit. Wenn wir die Markdown-Konvertierung einmal als API und einmal als React-Komponente bauen, laufen wir auch Gefahr, dass die Ergebnisse auseinanderlaufen.

Vermeidung von Wasserfällen

Kommen wir zu einem weiteren positiven Effekt, der sich aus der Verwendung von React Server Components ergeben kann. In React-Anwendungen ohne Server Components kommt es teilweise zu einem sogenannten „Waterfall“ im Browser. Stellen wir uns eine Komponente vor, die wir wie folgt verwenden, und die einen Artikel auf entwickler.de darstellt: <Article articleId={4711} />. Diese Komponente ist offenbar selbst dafür zuständig, die Artikeldaten zu laden, denn sie bekommt lediglich die ID des Artikels übergeben. Außerdem stellt sie irgendwie den Artikel dar, und zwar inklusive der Kommentare. Der Code der Komponente könnte wie in Listing 2 aussehen.

Listing 2

const Article = ({ articleId }) => {
  const [article, setArticle] = useState(null);
 
  useEffect(() => {
    fetchArticle(articleId).then((articleData) => {
      setArticle(articleData);
    });
  }, [articleId]);
 
  if (article === null) {
    return <LoadingSpinner />;
  }
 
  return (
    <>
      <ArticleDetails article={article} />
      <ArticleComments article={article} />
    </>
  );
};

Die Komponente verwendet also einen wiederverwendbaren Hook, um einen Artikel zu laden. Da wir vermutlich nicht überall, wo wir einen Artikel laden, auch alle Kommentare laden, muss die ArticleComments-Komponente selbst noch einmal die Kommentare laden. Sehen wir uns einmal an, was abläuft, wenn unsere Article-Komponente gerendert wird.

Im ersten Rendering wird article mit null initialisiert. Der in useEffect angegebene Effekt wird nicht direkt während des Renderns ausgeführt. Das bedeutet, article ist im ersten Rendering immer null. Die Komponente rendert also im ersten Rendering immer einen LoadingSpinner. Nach dem Rendering wird nun der Effekt ausgeführt, der die Daten vom Server lädt und asynchron nach dem erfolgreichen Laden den Article im State setzt. Durch das Ändern des State wird nun ein erneutes Rendering ausgeführt. In diesem Rendering ist der Article nicht mehr null und wir rendern die ArticleDetails– und die ArticleComments-Komponenten. Wenn nun die ArticleDetails-Komponente eventuell noch Informationen zum Autor des Article lädt, und die ArticleComments-Komponente beginnt, die Kommentare zu laden, so haben wir unseren Wasserfall – ein unerwünschtes sequenzielles Laden von Daten, die wir eigentlich gerne parallel geladen hätten. Die Seite baut sich dabei Stück für Stück auf und es kann passieren, dass Inhalte hin und her springen, wenn neue Daten verfügbar sind.

Stellen wir uns nun vor, wir würden exakt die gleiche Komponente als Server Component verwenden. Vorweg sei gesagt, dass das nicht funktionieren wird (dazu später mehr). Wir müssten die Komponente etwas anpassen, aber konzeptionell bleibt es sehr ähnlich, und daher spare ich uns diesen Exkurs. Auch serverseitig hätten wir einen Wasserfall, da erst die Artikeldaten und dann die Kommentare sowie Autordaten geladen werden. Serverseitig ist das aber üblicherweise ein deutlich geringeres Problem, da aufgrund der geringeren Latenz die einzelnen Aufrufe sehr viel schneller abgeschlossen werden können. Das Problem, dass die Inhalte nach und nach angezeigt werden verschwindet bei serverseitiger Ausführung komplett.

Es gibt natürlich auch andere Wege, solche Wasserfälle loszuwerden. Häufig ist das aber mit einer gewissen Umstellung der Architektur der React-Anwendung verbunden, die nicht jedem zusagt. Der oben dargestellte Weg wird manchmal auch als „Fetch-on-Render“ bezeichnet und ist relativ beliebt und weit verbreitet. Insofern ist es schön zu sehen, dass React Server Components das Problem verkleinern können.

Die Schattenseiten von React Server Components

Wie immer im Leben ist natürlich auch mit React Server Components nicht alles nur heile Welt und es ist wichtig, sich auch mit den Nachteilen der Technologie auseinanderzusetzen.

Fangen wir mal mit dem Offensichtlichen an: React Server Components sind bisher nur ein Vorschlag. Sie sind das Ergebnis längerer Forschungen bei Facebook, und die Antwort auf mehrere Probleme, die Facebook hatte. Das Team bei Facebook ist optimistisch, dass dieser neue Ansatz Probleme vieler Anwendungen lösen kann. Ein Datum, ab wann wir React Server Components in unseren Anwendungen einsetzen können, gibt es bisher nicht. Das React-Team geht aktuell davon aus, dass React Server Components zuerst in Frameworks wie Next.js Einzug halten werden und eine direkte Verwendung noch weiter entfernt ist. Aber auch dafür gibt es kein Releasedatum.

Außerdem ist es nicht so, dass jede unserer bestehenden Komponenten automatisch auch als React Server Component funktionieren wird. So funktionieren einige Hooks serverseitig nicht; z. B. werden useState oder useEffect in React Server Components nicht unterstützt. Deswegen funktioniert unsere obige Article-Komponente auch nicht als React Server Component. Wir können die gleiche Funktionalität aber auch als React Server Component implementieren, nur eben ohne diese Hooks. Diese Einschränkung impliziert übrigens auch, dass alle (eigenen) Hooks, die einen der nicht unterstützten Hooks verwenden, nicht in React Server Components verwendet werden können.

Das Gleiche gilt auch andersrum – nicht alle React Server Components können auch im Browser ausgeführt werden. Das liegt unter anderem daran, dass React Server Components auf APIs zugreifen können, die clientseitig nicht zur Verfügung stehen. Zum Beispiel können wir in React Server Components auf das Dateisystem des Servers zugreifen. Das geht aus dem Browser heraus glücklicherweise nicht.

Eine weitere Beschränkung ist, dass Server Components zwar Client Components (und native HTML-Elemente) rendern dürfen, Client Components aber wiederrum keine Server Components. Es ist allerdings durchaus möglich, aus einer React Server Component weitere gerenderte React Server Components als prop an eine Clientkomponente zu übergeben, zum Beispiel als children.

Interessant ist übrigens, dass, wenn wir eine Client Component innerhalb einer Server Component verwenden, das Rendering der Client Component regulär auf dem Client stattfindet. Sollte die Server Component neue Daten an die Client Component übermitteln wollen, so werden diese per HTTP übertragen und an die Client Component gegeben – diese rendert sich dann neu, verliert aber ihren State nicht.

Fazit und Ausblick

Server Components könnten tatsächlich die Verbreitung von React noch einmal beschleunigen, da das Framework so auch in Anwendungen eingesetzt werden kann, in denen es bisher nicht möglich war. Sie ermöglichen es außerdem, dass Frontend-Entwickler, die bisher wenig Einblick in das Backend hatten, auf eine ihnen bekannte Art und Weise auch Teile des Backends entwickeln können. Das ist eine Hürde, die man als Fullstack-Entwickler häufig aus den Augen verliert. Trotzdem bewegen wir uns hier in eine Richtung, die es Frontend-Entwicklern ermöglicht, einen größeren Teil der Verantwortung für die komplette Benutzeroberfläche zu übernehmen.

Auch die Möglichkeit, den gleichen Code je nach Situation serverseitig oder clientseitig auszuführen ist etwas, dass ich für ein sehr spannendes Konzept halte. Es ermöglicht interessante Optionen, beispielsweise für komplexere Algorithmen, bei denen ggf. auch noch Trainingsdaten hinzukommen. Trainingsdaten sind teilweise sehr umfangreich und lassen das herunterzuladende JavaScript klein aussehen. Man könnte zum Beispiel den Algorithmus serverseitig ausführen, solange die Trainingsdaten noch geladen werden. Erst, wenn alles offline im Browser bereitsteht, würde man die Berechnung auf den Client auslagern, wodurch sich die Performance verbessern dürfte und wir außerdem serverseitig Rechenkapazitäten sparen – und unsere Anwendung gegebenenfalls sogar offlinefähig wird.

Grundsätzlich ist das natürlich alles trotzdem keine revolutionäre neue Entwicklung. Viele der „Vorteile“, die man durch Server Components gewinnt, besitzt eine traditionelle Webanwendung ohne JavaScript im Browser von Haus aus. Facebook sagt auch explizit, dass sie sich von solchen traditionellen Webarchitekturen haben inspirieren lassen. Nichtsdestotrotz – React Server Components erlauben uns, das Beste aus beiden Welten zu bekommen. Deswegen bin ich sehr gespannt darauf, React Server Components irgendwann in der Zukunft einmal in einer echten Anwendung zu erleben. Akademischer Ghostwriter hat diesen Text mitgestaltet.

Immer auf dem Laufenden bleiben!
Alle News & Updates: