Heim >Web-Frontend >js-Tutorial >Erweiterte React SSR-Techniken mit Streaming und dynamischen Daten

Erweiterte React SSR-Techniken mit Streaming und dynamischen Daten

Susan Sarandon
Susan SarandonOriginal
2025-01-05 03:49:38264Durchsuche

Advanced React SSR Techniques with Streaming and Dynamic Data

Wenn Ihre Anwendung wächst, wachsen auch die Herausforderungen. Um an der Spitze zu bleiben, ist die Beherrschung fortschrittlicher SSR-Techniken für die Bereitstellung eines nahtlosen und leistungsstarken Benutzererlebnisses unerlässlich.

Nachdem ich im vorherigen Artikel eine Grundlage für serverseitiges Rendering in React-Projekten geschaffen habe, freue ich mich, Ihnen Funktionen vorstellen zu können, die Ihnen dabei helfen können, die Skalierbarkeit von Projekten aufrechtzuerhalten, Daten effizient vom Server auf den Client zu laden und Hydratationsprobleme zu lösen.

Inhaltsverzeichnis

  • Was ist Streaming in SSR?
  • Lazy Loading und SSR
  • Implementierung von Streaming mit Lazy Loading
    • React-Komponenten aktualisieren
    • Aktualisieren des Servers für Streaming
  • Server-zu-Client-Daten
    • Übergabe von Daten an den Server
    • Umgebungsvariablen auf dem Client verarbeiten
  • Probleme mit der Flüssigkeitszufuhr
    • Beispielszenario
    • Behebung von Flüssigkeitsproblemen
  • Fazit

Was ist Streaming in SSR?

Streaming beim serverseitigen Rendering (SSR) ist eine Technik, bei der der Server Teile der HTML-Seite in Blöcken an den Browser sendet, während sie generiert werden, anstatt darauf zu warten, dass die gesamte Seite fertig ist bevor Sie es ausliefern. Dadurch kann der Browser sofort mit dem Rendern von Inhalten beginnen, was die Ladezeiten und die Leistung des Benutzers verbessert.

Streaming ist besonders effektiv für:

  • Große Seiten: Das Generieren des gesamten HTML-Codes kann viel Zeit in Anspruch nehmen.
  • Dynamischer Inhalt: Wenn Teile der Seite von externen API-Aufrufen oder dynamisch generierten Blöcken abhängen.
  • Anwendungen mit hohem Datenverkehr: Zur Reduzierung der Serverlast und Latenz bei Spitzenauslastung.

Streaming schließt die Lücke zwischen traditionellem SSR und moderner clientseitiger Interaktivität und stellt sicher, dass Benutzer aussagekräftige Inhalte schneller sehen, ohne Kompromisse bei der Leistung einzugehen.

Lazy Loading und SSR

Lazy Loading ist eine Technik, die das Laden von Komponenten oder Modulen verzögert, bis sie tatsächlich benötigt werden, wodurch die anfängliche Ladezeit verkürzt und die Leistung verbessert wird. In Kombination mit SSR kann Lazy Loading sowohl die Server- als auch die Client-Arbeitslast erheblich optimieren.

Lazy Loading basiert auf React.lazy, das Komponenten dynamisch als Promises importiert. Im herkömmlichen SSR erfolgt das Rendering synchron, was bedeutet, dass der Server alle Promises auflösen muss, bevor er den vollständigen HTML-Code generiert und an den Browser sendet.

Streaming löst diese Herausforderungen, indem es dem Server ermöglicht, HTML in Blöcken zu senden, während Komponenten gerendert werden. Durch diesen Ansatz kann der Suspense-Fallback sofort an den Browser gesendet werden, sodass Benutzer frühzeitig aussagekräftige Inhalte sehen. Wenn verzögert geladene Komponenten aufgelöst werden, wird ihr gerenderter HTML-Code schrittweise an den Browser gestreamt und ersetzt so nahtlos den Fallback-Inhalt. Dadurch wird eine Blockierung des Rendering-Prozesses vermieden, Verzögerungen reduziert und die wahrgenommene Ladezeit verbessert.

Implementierung von Streaming mit Lazy Loading

Dieser Leitfaden baut auf Konzepten auf, die im vorherigen Artikel Erstellung produktionsbereiter SSR-React-Anwendungen vorgestellt wurden, den Sie unten verlinkt finden. Um SSR mit React zu ermöglichen und verzögert geladene Komponenten zu unterstützen, werden wir mehrere Updates sowohl an den React-Komponenten als auch am Server vornehmen.

Aktualisieren von Reaktionskomponenten

Server-Einstiegspunkt

Die renderToString-Methode von React wird üblicherweise für SSR verwendet, sie wartet jedoch, bis der gesamte HTML-Inhalt fertig ist, bevor sie ihn an den Browser sendet. Durch den Wechsel zu renderToPipeableStream können wir Streaming aktivieren, das Teile des HTML sendet, während sie generiert werden.

// ./src/entry-server.tsx
import { renderToPipeableStream, RenderToPipeableStreamOptions } from 'react-dom/server'
import App from './App'

export function render(options?: RenderToPipeableStreamOptions) {
  return renderToPipeableStream(<App />, options)
}

Erstellen einer Lazy-Loaded-Komponente

In diesem Beispiel erstellen wir eine einfache Kartenkomponente, um das Konzept zu demonstrieren. In Produktionsanwendungen wird diese Technik typischerweise bei größeren Modulen oder ganzen Seiten verwendet, um die Leistung zu optimieren.

// ./src/Card.tsx
import { useState } from 'react'

function Card() {
  const [count, setCount] = useState(0)

  return (
    <div className="card">
      <button onClick={() => setCount((count) => count + 1)}>
        count is {count}
      </button>
      <p>
        Edit <code>src/App.tsx</code> and save to test HMR
      </p>
    </div>
  )
}

export default Card

Verwenden der Lazy-Loaded-Komponente in der App

Um die Lazy-Loaded-Komponente zu verwenden, importieren Sie sie dynamisch mit React.lazy und umschließen Sie sie mit Suspense, um während des Ladens eine Fallback-Benutzeroberfläche bereitzustellen

// ./src/App.tsx
import { lazy, Suspense } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

const Card = lazy(() => import('./Card'))

function App() {
  return (
    <>
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <Suspense fallback='Loading...'>
        <Card />
      </Suspense>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  )
}

export default App

Aktualisieren des Servers für Streaming

Um Streaming zu ermöglichen, müssen sowohl die Entwicklungs- als auch die Produktionseinstellungen einen konsistenten HTML-Rendering-Prozess unterstützen. Da der Prozess für beide Umgebungen derselbe ist, können Sie eine einzige wiederverwendbare Funktion erstellen, um Streaming-Inhalte effektiv zu verarbeiten.

Erstellen einer Stream-Content-Funktion

// ./server/constants.ts
export const ABORT_DELAY = 5000

Die streamContent-Funktion initiiert den Rendering-Prozess, schreibt inkrementelle HTML-Blöcke in die Antwort und stellt eine ordnungsgemäße Fehlerbehandlung sicher.

// ./server/streamContent.ts
import { Transform } from 'node:stream'
import { Request, Response, NextFunction } from 'express'
import { ABORT_DELAY, HTML_KEY } from './constants'
import type { render } from '../src/entry-server'

export type StreamContentArgs = {
  render: typeof render
  html: string
  req: Request
  res: Response
  next: NextFunction
}

export function streamContent({ render, html, res }: StreamContentArgs) {
  let renderFailed = false

  // Initiates the streaming process by calling the render function
  const { pipe, abort } = render({
    // Handles errors that occur before the shell is ready
    onShellError() {
      res.status(500).set({ 'Content-Type': 'text/html' }).send('<pre class="brush:php;toolbar:false">Something went wrong
') }, // Called when the shell (initial HTML) is ready for streaming onShellReady() { res.status(renderFailed ? 500 : 200).set({ 'Content-Type': 'text/html' }) // Split the HTML into two parts using the placeholder const [htmlStart, htmlEnd] = html.split(HTML_KEY) // Write the starting part of the HTML to the response res.write(htmlStart) // Create a transform stream to handle the chunks of HTML from the renderer const transformStream = new Transform({ transform(chunk, encoding, callback) { // Write each chunk to the response res.write(chunk, encoding) callback() }, }) // When the streaming is finished, write the closing part of the HTML transformStream.on('finish', () => { res.end(htmlEnd) }) // Pipe the render output through the transform stream pipe(transformStream) }, onError(error) { // Logs errors encountered during rendering renderFailed = true console.error((error as Error).stack) }, }) // Abort the rendering process after a delay to avoid hanging requests setTimeout(abort, ABORT_DELAY) }

Entwicklungskonfiguration aktualisieren

// ./server/dev.ts
import { Application } from 'express'
import fs from 'fs'
import path from 'path'
import { StreamContentArgs } from './streamContent'

const HTML_PATH = path.resolve(process.cwd(), 'index.html')
const ENTRY_SERVER_PATH = path.resolve(process.cwd(), 'src/entry-server.tsx')

// Add to args the streamContent callback
export async function setupDev(app: Application, streamContent: (args: StreamContentArgs) => void) {
  const vite = await (
    await import('vite')
  ).createServer({
    root: process.cwd(),
    server: { middlewareMode: true },
    appType: 'custom',
  })

  app.use(vite.middlewares)

  app.get('*', async (req, res, next) => {
    try {
      let html = fs.readFileSync(HTML_PATH, 'utf-8')
      html = await vite.transformIndexHtml(req.originalUrl, html)

      const { render } = await vite.ssrLoadModule(ENTRY_SERVER_PATH)

      // Use the same callback for production and development process
      streamContent({ render, html, req, res, next })
    } catch (e) {
      vite.ssrFixStacktrace(e as Error)
      console.error((e as Error).stack)
      next(e)
    }
  })
}

Aktualisierung der Produktionskonfiguration

// ./server/prod.ts
import { Application } from 'express'
import fs from 'fs'
import path from 'path'
import compression from 'compression'
import sirv from 'sirv'
import { StreamContentArgs } from './streamContent'

const CLIENT_PATH = path.resolve(process.cwd(), 'dist/client')
const HTML_PATH = path.resolve(process.cwd(), 'dist/client/index.html')
const ENTRY_SERVER_PATH = path.resolve(process.cwd(), 'dist/ssr/entry-server.js')

// Add to Args the streamContent callback
export async function setupProd(app: Application, streamContent: (args: StreamContentArgs) => void) {
  app.use(compression())
  app.use(sirv(CLIENT_PATH, { extensions: [] }))

  app.get('*', async (req, res, next) => {
    try {
      const html = fs.readFileSync(HTML_PATH, 'utf-8')

      const { render } = await import(ENTRY_SERVER_PATH)

      // Use the same callback for production and development process
      streamContent({ render, html, req, res, next })
    } catch (e) {
      console.error((e as Error).stack)
      next(e)
    }
  })
}

Aktualisieren des Express-Servers

Übergeben Sie die streamContent-Funktion an jede Konfiguration:

// ./server/app.ts
import express from 'express'
import { PROD, APP_PORT } from './constants'
import { setupProd } from './prod'
import { setupDev } from './dev'
import { streamContent } from './streamContent'

export async function createServer() {
  const app = express()

  if (PROD) {
    await setupProd(app, streamContent)
  } else {
    await setupDev(app, streamContent)
  }

  app.listen(APP_PORT, () => {
    console.log(`http://localhost:${APP_PORT}`)
  })
}

createServer()

Nach der Implementierung dieser Änderungen wird Ihr Server:

  • Streamen Sie HTML schrittweise an den Browser und verkürzen Sie so die Zeit bis zum ersten Malen.
  • Verarbeiten Sie nahtlos geladene Komponenten und verbessern Sie so sowohl die Leistung als auch das Benutzererlebnis.

Server-zu-Client-Daten

Bevor Sie HTML an den Client senden, haben Sie die volle Kontrolle über das vom Server generierte HTML. Dadurch können Sie die Struktur dynamisch ändern, indem Sie nach Bedarf Tags, Stile, Links oder andere Elemente hinzufügen.

Eine besonders wirkungsvolle Technik ist das Einfügen eines