18.5.2022 |

Suspense + HTTP Streaming in React 18

React hat schon länger die Möglichkeit, per SSR Seiten auf dem Server vorzurendern um Seitenladezeiten zu verringern. Bisher brachte dies allerdings einige Probleme mit sich:

  • der Server muss warten, bis alle Daten geladen sind, bevor das HTML gerendert werden kann. Bei langsamen Datenbank-Requests verzögert sich damit der gesamte Request.
  • Dadurch dauert es auch länger, bis angefangen wird, die JavaScript-Bundles herunterzuladen.
  • SSR kann also unter Umständen sogar Seitenladezeiten erhöhen.

Suspense

Seit React 17 gibt es die <Suspense> component.

          <Suspense key={id} fallback={<Spinner />}>
            <StoryWithData id={id} />
          </Suspense>
function StoryWithData({ id }) {
  const data = useData(`s-${id}`, () =>
    fetchData(`item/${id}`, Math.random() * 2000).then(transform)
  )
  return <Story {...data} />
}
const cache = {}

export default function useData(key, fetcher) {
  if (!cache[key]) {
    let data
    let promise
    cache[key] = () => {
      if (data !== undefined) return data
      if (!promise) promise = fetcher().then((r) => (data = r))
      throw promise
    }
  }
  return cache[key]()
}

Die Suspense Component zeigt solange den Spinner an, bis der useData hook die Daten geladen hat. Intern funktioniert dies interessanterweise mit einem throw / catch. Nur, dass kein Fehler geworfen wird, sondern ein Promise. Die Suspense component catched dann das Promise und weiß, dass die component noch am Laden ist. Sobald die component erfolgreich geladen ist, wird sie erneut gerendert, diesmal mit den richtigen Daten. Zur veranschaulichung wurde die useData hook hier selbst implementiert, normalerweise würde das durch eine library wie zum beispiel swr oder apollo intern geregelt.

Der größte Vorteil am Suspense Pattern ist, dass sich components so nicht mehr selbst darum kümmern müssen, eine Fortschrittsanzeige zu implementieren. Ein weiterer Vorteil ist, dass es SSR Streaming erlaubt:

SSR Streaming

Mit React 17 SSR wird auf dem Server das html mit renderToString(<App/>) gerendert.
In React 18 wird es eine renderToPipeableStream(<App/>) Funktion geben, die die App als stream rendert und immer wenn ein suspense fertig ist, ein neuen teil in den stream schickt. Dank der Suspense Components kann React nun die App Stück für Stück rendern. Dank etwas inline JavaScript baut React dann die App im Browser während des ladens zusammen. So wird im Browser schon ein Teil der App angezeigt, bevor überhaupt der letzte Datenbankrequest ausgeführt wurde.

Zu Demonstrationszwecken habe ich in dem Beispielprojekt mal den header hinter den body gesetzt, damit die JavaScript-Bundles zuletzt laden. Man kann schön sehen, wie die App sich Stück für Stück zusammenbaut, alles in dem einen initialen HTTP Request.

Bildschirmaufnahme_2021-12-08_um_11.11.42

Zum Vergleich, bei React 17 wäre die Seite die ersten 2 Sekunden weiß geblieben, bis alle Datenbank-Requests beendet sind.

Quellen: https://github.com/reactwg/react-18/discussions/37 https://nextjs.org/docs/advanced-features/react-18

Zur Übersicht

Mehr vom DevSquad...

Jan Sauer

How does Spring Boot handle shutdowns

Sophia Brandt

Patterns.dev - ein hilfreiches kostenloses Nachschlagewerk für React-Entwickler:innen