À mesure que votre candidature grandit, les défis évoluent également. Pour garder une longueur d'avance, la maîtrise des techniques SSR avancées est essentielle pour offrir une expérience utilisateur fluide et performante.
Après avoir jeté les bases du rendu côté serveur dans les projets React dans l'article précédent, je suis ravi de partager des fonctionnalités qui peuvent vous aider à maintenir l'évolutivité du projet, à charger efficacement les données du serveur vers le client et à résoudre les problèmes d'hydratation.
Le streaming dans le rendu côté serveur (SSR) est une technique dans laquelle le serveur envoie des parties de la page HTML au navigateur par morceaux au fur et à mesure de leur génération, plutôt que d'attendre que la page entière soit prête. avant de le livrer. Cela permet au navigateur de commencer à restituer le contenu immédiatement, améliorant ainsi les temps de chargement et les performances de l'utilisateur.
Le streaming est particulièrement efficace pour :
Le streaming comble le fossé entre le SSR traditionnel et l'interactivité moderne côté client, garantissant aux utilisateurs de voir un contenu significatif plus rapidement sans compromettre les performances.
Le chargement paresseux est une technique qui diffère le chargement de composants ou de modules jusqu'à ce qu'ils soient réellement nécessaires, réduisant ainsi le temps de chargement initial et améliorant les performances. Lorsqu'il est combiné avec SSR, le chargement paresseux peut optimiser considérablement les charges de travail du serveur et du client.
Le chargement paresseux repose sur React.lazy, qui importe dynamiquement des composants sous forme de promesses. Dans le SSR traditionnel, le le rendu est synchrone, ce qui signifie que le serveur doit résoudre toutes les promesses avant de générer et d'envoyer le code HTML complet au navigateur.
Le streaming résout ces problèmes en permettant au serveur d'envoyer du HTML par morceaux au fur et à mesure du rendu des composants. Cette approche permet d'envoyer immédiatement la solution de secours Suspense au navigateur, garantissant ainsi aux utilisateurs de voir rapidement un contenu significatif. Au fur et à mesure que les composants chargés paresseux sont résolus, leur HTML rendu est diffusé progressivement vers le navigateur, remplaçant de manière transparente le contenu de secours. Cela évite de bloquer le processus de rendu, réduit les retards et améliore les temps de chargement perçus.
Ce guide s'appuie sur les concepts introduits dans l'article précédent, Création d'applications SSR React prêtes pour la production, dont vous pouvez trouver le lien en bas. Pour activer SSR avec React et prendre en charge les composants à chargement différé, nous effectuerons plusieurs mises à jour des composants React et du serveur.
La méthode renderToString de React est couramment utilisée pour SSR, mais elle attend que l'intégralité du contenu HTML soit prête avant de l'envoyer au navigateur. En passant à renderToPipeableStream, nous pouvons activer le streaming, qui envoie des parties du HTML au fur et à mesure de leur génération.
// ./src/entry-server.tsx import { renderToPipeableStream, RenderToPipeableStreamOptions } from 'react-dom/server' import App from './App' export function render(options?: RenderToPipeableStreamOptions) { return renderToPipeableStream(<App />, options) }
Dans cet exemple, nous allons créer un composant Card simple pour démontrer le concept. Dans les applications de production, cette technique est généralement utilisée avec des modules plus grands ou des pages entières pour optimiser les performances.
// ./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
Pour utiliser le composant chargé paresseux, importez-le dynamiquement à l'aide de React.lazy et enveloppez-le avec Suspense pour fournir une interface utilisateur de secours pendant le chargement
// ./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
Pour permettre le streaming, les configurations de développement et de production doivent prendre en charge un processus de rendu HTML cohérent. Étant donné que le processus est le même pour les deux environnements, vous pouvez créer une seule fonction réutilisable pour gérer efficacement le contenu en streaming.
// ./server/constants.ts export const ABORT_DELAY = 5000
La fonction streamContent lance le processus de rendu, écrit des morceaux incrémentiels de code HTML dans la réponse et garantit une gestion appropriée des erreurs.
// ./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) } }) }
Passez la fonction streamContent à chaque configuration :
// ./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()
Après avoir mis en œuvre ces modifications, votre serveur :
Avant d'envoyer du HTML au client, vous avez un contrôle total sur le HTML généré par le serveur. Cela vous permet de modifier dynamiquement la structure en ajoutant des balises, des styles, des liens ou tout autre élément selon vos besoins.
Une technique particulièrement puissante consiste à injecter un