ホームページ >ウェブフロントエンド >jsチュートリアル >ストリーミングおよび動的データを使用した高度な React SSR テクニック
アプリケーションが成長するにつれて、課題も増加します。先を行くには、シームレスで高性能のユーザー エクスペリエンスを提供するために、高度な SSR テクニックを習得することが不可欠です。
前の記事で React プロジェクトでサーバー側レンダリングの基盤を構築したので、プロジェクトのスケーラビリティを維持し、サーバーからクライアントにデータを効率的にロードし、ハイドレーションの問題を解決するのに役立つ機能を共有できることを嬉しく思います。
サーバーサイド レンダリング (SSR) でのストリーミング は、ページ全体の準備が整うのを待つのではなく、サーバーが HTML ページの一部を生成時に分割してブラウザーに送信する手法です。届ける前に。これにより、ブラウザはコンテンツのレンダリングをすぐに開始できるようになり、読み込み時間とユーザーのパフォーマンスが向上します。
ストリーミングは次の場合に特に効果的です。
ストリーミングは、従来の SSR と最新のクライアント側の対話性の間のギャップを埋め、ユーザーがパフォーマンスを犠牲にすることなく意味のあるコンテンツをより速く表示できるようにします。
遅延読み込み は、実際に必要になるまでコンポーネントまたはモジュールの読み込みを延期し、初期読み込み時間を短縮し、パフォーマンスを向上させる手法です。 SSR と組み合わせると、遅延読み込みはサーバーとクライアントの両方のワークロードを大幅に最適化できます。
遅延読み込みは、コンポーネントを Promise として動的にインポートする React.lazy に依存しています。従来の SSR では、レンダリングは同期的です。つまり、サーバーは完全な HTML を生成してブラウザに送信する前に、すべての Promises を解決する必要があります。
ストリーミングは、コンポーネントがレンダリングされるときにサーバーが HTML をチャンクで送信できるようにすることで、これらの課題を解決します。このアプローチにより、サスペンス フォールバックをブラウザーに即座に送信できるようになり、ユーザーが意味のあるコンテンツを早期に確認できるようになります。遅延ロードされたコンポーネントが解決されると、レンダリングされた HTML が段階的にブラウザにストリーミングされ、フォールバック コンテンツがシームレスに置き換えられます。これにより、レンダリング プロセスのブロックが回避され、遅延が減少し、体感的な読み込み時間が短縮されます。
このガイドは、下部にリンクされている前の記事 本番対応 SSR React アプリケーションの構築 で紹介した概念に基づいて構築されています。 React で SSR を有効にし、遅延読み込みコンポーネントをサポートするために、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) } }) }
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 を完全に制御できます。これにより、必要に応じてタグ、スタイル、リンク、またはその他の要素を追加して、構造を動的に変更できます。
特に強力な手法の 1 つは、