ホームページ >ウェブフロントエンド >jsチュートリアル >ストリーミングおよび動的データを使用した高度な React SSR テクニック

ストリーミングおよび動的データを使用した高度な React SSR テクニック

Susan Sarandon
Susan Sarandonオリジナル
2025-01-05 03:49:38266ブラウズ

Advanced React SSR Techniques with Streaming and Dynamic Data

アプリケーションが成長するにつれて、課題も増加します。先を行くには、シームレスで高性能のユーザー エクスペリエンスを提供するために、高度な SSR テクニックを習得することが不可欠です。

前の記事で React プロジェクトでサーバー側レンダリングの基盤を構築したので、プロジェクトのスケーラビリティを維持し、サーバーからクライアントにデータを効率的にロードし、ハイドレーションの問題を解決するのに役立つ機能を共有できることを嬉しく思います。

目次

  • SSR のストリーミングとは
  • 遅延読み込みと SSR
  • 遅延読み込みによるストリーミングの実装
    • React コンポーネントを更新しています
    • ストリーミング用サーバーの更新
  • サーバーからクライアントへのデータ
    • サーバー上でデータを渡す
    • クライアントで環境変数を処理する
  • 水分補給の問題
    • シナリオ例
    • 水分補給の問題を解決する
  • 結論

SSRのストリーミングとは何ですか

サーバーサイド レンダリング (SSR) でのストリーミング は、ページ全体の準備が整うのを待つのではなく、サーバーが HTML ページの一部を生成時に分割してブラウザーに送信する手法です。届ける前に。これにより、ブラウザはコンテンツのレンダリングをすぐに開始できるようになり、読み込み時間とユーザーのパフォーマンスが向上します。

ストリーミングは次の場合に特に効果的です。

  • 大きなページ: HTML 全体の生成にかなりの時間がかかる可能性があります。
  • 動的コンテンツ: ページの一部が外部 API 呼び出しまたは動的に生成されたチャンクに依存する場合。
  • 高トラフィック アプリケーション: ピーク使用時のサーバーの負荷と遅延を軽減します。

ストリーミングは、従来の SSR と最新のクライアント側の対話性の間のギャップを埋め、ユーザーがパフォーマンスを犠牲にすることなく意味のあるコンテンツをより速く表示できるようにします。

遅延読み込みと SSR

遅延読み込み は、実際に必要になるまでコンポーネントまたはモジュールの読み込みを延期し、初期読み込み時間を短縮し、パフォーマンスを向上させる手法です。 SSR と組み合わせると、遅延読み込みはサーバーとクライアントの両方のワークロードを大幅に最適化できます。

遅延読み込みは、コンポーネントを Promise として動的にインポートする React.lazy に依存しています。従来の SSR では、レンダリングは同期的です。つまり、サーバーは完全な HTML を生成してブラウザに送信する前に、すべての Promises を解決する必要があります。

ストリーミングは、コンポーネントがレンダリングされるときにサーバーが HTML をチャンクで送信できるようにすることで、これらの課題を解決します。このアプローチにより、サスペンス フォールバックをブラウザーに即座に送信できるようになり、ユーザーが意味のあるコンテンツを早期に確認できるようになります。遅延ロードされたコンポーネントが解決されると、レンダリングされた HTML が段階的にブラウザにストリーミングされ、フォールバック コンテンツがシームレスに置き換えられます。これにより、レンダリング プロセスのブロックが回避され、遅延が減少し、体感的な読み込み時間が短縮されます。

遅延読み込みによるストリーミングの実装

このガイドは、下部にリンクされている前の記事 本番対応 SSR React アプリケーションの構築 で紹介した概念に基づいて構築されています。 React で SSR を有効にし、遅延読み込みコンポーネントをサポートするために、React コンポーネントとサーバーの両方にいくつかの更新を加えます。

React コンポーネントの更新

サーバーエントリーポイント

React の renderToString メソッドは SSR によく使用されますが、HTML コンテンツ全体の準備が整うまで待ってからブラウザに送信します。 renderToPipeableStream に切り替えることで、生成された HTML の一部を送信するストリーミングを有効にすることができます。

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

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

遅延ロードコンポーネントの作成

この例では、概念を示すために単純な Card コンポーネントを作成します。運用アプリケーションでは、この手法は通常、パフォーマンスを最適化するために、より大きなモジュールまたはページ全体で使用されます。

// ./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

アプリでの遅延ロードコンポーネントの使用

遅延読み込みコンポーネントを使用するには、React.lazy を使用してコンポーネントを動的にインポートし、Suspense でラップして読み込み中にフォールバック UI を提供します

// ./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

ストリーミング用にサーバーを更新する

ストリーミングを有効にするには、開発セットアップと運用セットアップの両方が一貫した HTML レンダリング プロセスをサポートする必要があります。どちらの環境でもプロセスは同じであるため、単一の再利用可能な関数を作成して、ストリーミング コンテンツを効果的に処理できます。

ストリームコンテンツ関数の作成

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

streamContent 関数は、レンダリング プロセスを開始し、HTML の増分チャンクを応答に書き込み、適切なエラー処理を保証します。

// ./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) }

開発構成の更新

// ./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)
    }
  })
}

実稼働構成の更新

// ./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)
    }
  })
}

Express サーバーの更新

streamContent 関数を各構成に渡します:

// ./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()

これらの変更を実装すると、サーバーは次のようになります:

  • HTML を段階的にブラウザにストリーミングし、最初の描画にかかる時間を短縮します。
  • 遅延ロードされたコンポーネントをシームレスに処理し、パフォーマンスとユーザー エクスペリエンスの両方を向上させます。

サーバーからクライアントへのデータ

HTML をクライアントに送信する前に、サーバーで生成された HTML を完全に制御できます。これにより、必要に応じてタグ、スタイル、リンク、またはその他の要素を追加して、構造を動的に変更できます。

特に強力な手法の 1 つは、