React 18 でのサーバーサイド レンダリング

Feb 04 2023
NextJS や他のフレームワークなし。
React は、JavaScript に大きく依存するシングル ページ アプリケーション (SPA) の開発に一般的に使用されるライブラリです。実際、ブラウザからリクエストが行われると、React は空の HTML ページを返し、最初のページの読み込み後に JavaScript を使用してコンテンツが実際に読み込まれます。
Unsplash の羽鳥史朗による写真

React は、JavaScript に大きく依存するシングル ページ アプリケーション (SPA) の開発に一般的に使用されるライブラリです。

実際、ブラウザからリクエストが行われると、React は空の HTML ページを返し、最初のページの読み込み後に JavaScript を使用してコンテンツが実際に読み込まれます。

このアプローチは機能しますが、考慮すべき重大な欠点がいくつかあります。

  • React から返される最初の HTML が空であるため、検索エンジン最適化 (SEO) に悪影響を及ぼし、検索エンジンでのランク付けが困難になります。
  • 大きな JavaScript バンドルを取得するため、ページの最初のレンダリングの待ち時間が長くなる可能性があります。

コードに直接ジャンプしたい場合は、Github でコードをチェックアウトしてください。https://github.com/rajgaur98/react-ssr/tree/main。

サーバーサイドレンダリング

サーバーサイド レンダリング (SSR) は、サーバー上で Web ページをレンダリングし、クライアントに送り返す技術です。

SSR は、完全にレンダリングされた HTML ページをブラウザーに返します。これにより、劣悪な SEO や最初のページのレンダリングの長い待ち時間など、前述の問題を軽減できます。

NextJS のようなフレームワークは、すぐに SSR をすでにサポートしていますが、最初から実装することも可能です。このプロセスには 3 つのステップが含まれます。

  1. サーバー上でHTMLを生成する
  2. サーバーから受信したHTMLをクライアント上でレンダリングする
  3. クライアント上の静的 HTML への JavaScript の追加 (ハイドレーションとも呼ばれます)

設定

Web サーバーのセットアップにはExpressJS を使用し、もちろんフロントエンドには React と ReactDOM を使用します。

さらに、モジュール コードをブラウザが理解できるスクリプトにトランスパイルしてバンドルするために、 BabelとWebpackも必要になります。

必要な依存関係をインストールするには、次のコマンドを実行します。

yarn add express react react-dom

yarn add -D @babel/core @babel/preset-env @babel/preset-react @webpack-cli/generators babel-loader webpack webpack-cli

すべてのフロントエンド ファイルをclientルート フォルダー内のフォルダーに保存します。このフォルダーには典型的な React アプリケーションが含まれており、難しいものは何もありません。

index.jsxReact アプリのルートとして機能するファイルを作成しましょう。

import React from "react";
import { hydrateRoot } from "react-dom/client";
import { App } from "./App.jsx";

hydrateRoot(document, <App />);

次に進む前に、水分補給についてもっと理解しましょう。

サーバー上で Web ページをレンダリングすると、サーバーは静的なプレーン HTML ドキュメントを出力します。

これは最初のレンダリングをユーザーに表示するのに適していますが、サーバーはイベント リスナーや JavaScript をコードに付加しません。静的なページのみを返します。

このサーバーでレンダリングされた HTML に JavaScript を添付するのは、フロントエンドの役割です。したがって、この静的でドライなページに JavaScript をアタッチするプロセスは、ハイドレーションと呼ばれます。

したがって、コードの最後の行からわかるように、水和物ルート関数を利用して、コンポーネントdocumentを使用してサーバーレンダリングされた JavaScript をアタッチしていますApp。

次に、App コンポーネントを作成しましょう。

import React from "react";

export const App = () => {
  return (
    <html>
      <head>
      </head>
      <body>
        <div id="root">App</div>
      </body>
    </html>
  );
};

その理由は、サーバーが HTML 構造を使用して、必要な JavaScript および CSS インポートをスクリプトおよびリンク タグの形式で HTML に挿入できるためです。

後でアプリにさらに複雑さを追加するため、今のところは、サーバー側でこの単純なアプリをレンダリングすることに集中しましょう。

資産の管理

サーバー側レンダリングに CSS などのアセットを含めるには、HTML にlinkタグを追加するか、Webpack プラグインを使用します。head

たとえば、 と を使用してcss-loaderファイルMiniCssExtractPluginから CSS を抽出し.jsx、タグとして挿入できますlink。.jsxこれらのプラグインを使用すると、次のようにファイル内で CSS インポートを使用できます。

import React from "react"

import "../styles/main.css"

<head>
  <link rel="stylesheet" href="styles/Root.css"></link>
</head>

<img src="optimus.png" alt="Profile Picture" />

module: {
 rules: [
  {
    test: /\.(png|svg|jpg|jpeg|gif)$/i,
    type: 'asset/resource',
  },
 ],
},

srcサーバー ファイルはルート ディレクトリ内のフォルダーに保存します。サーバーは単純で、index.jsと の2 つのファイルで構成されていますrender.js。

index.js単一のルートを持つ標準の Express コードが含まれています。

const express = require("express");
const path = require("path");
const app = express();
const { render } = require("./render");

app.use(express.static(path.resolve(__dirname, "../build")));
app.use(express.static(path.resolve(__dirname, "../assets")));

app.get("/", (req, res) => {
  render(res);
});

app.listen(3000, () => {
  console.log("listening on port 3000");
});

import React from "react";
import { renderToPipeableStream } from "react-dom/server";
import { App } from "../client/App.jsx";

export const render = (response) => {
  const stream = renderToPipeableStream(<App />, {
    bootstrapScripts: ["client.bundle.js"],
    onShellReady() {
      response.setHeader("content-type", "text/html");
      stream.pipe(response);
    },
  });
};

上部の React インポートが必要です。そうしないと、Web ページがレンダリングされず、エラーがスローされます。

次にrenderToPipeableStream、コンポーネントを HTML にレンダリングするために使用される をインポートしますApp。

このrenderToPipeableStream関数は、コンポーネントとオプションという 2 つのパラメータを取りますApp。

このbootstrapScriptsオプションは、HTML をハイドレートするスクリプトへのパスを含む配列です。

スクリプトは、エントリ ポイントclient.bundle.jsの出力バンドルですclient/index.jsx。思い出していただければ、このhydrateRoot関数は の中にありますclient/index.jsx。

このonShellReady関数は、最初の HTML の準備ができた後にトリガーされます。stream.pipe(response)HTMLをブラウザに徐々に送信するために、HTML のストリーミングを開始できます。

シンプルな SSR アプリが完成し、すぐに実行できるようになりました。ただし、すべてのコードをそのまま実行するとエラーが発生します。まずクライアントとサーバーのコードをバンドルする必要があります。

モジュールのバンドル

コードをトランスパイルしてバンドルするには、ルート フォルダーに設定ファイルがbabel.config.json必要です。webpack.config.js

// babel.config.json

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

// webpack.config.js

const path = require("path");

const clientConfig = {
  target: "web",
  entry: "./client/index.jsx",
  output: {
    filename: "client.bundle.js",
    path: path.resolve(__dirname, "build"),
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/i,
        loader: "babel-loader",
      },
    ],
  },
};

const serverConfig = {
  target: "node",
  entry: "./src/index.js",
  output: {
    filename: "server.bundle.js",
    path: path.resolve(__dirname, "build"),
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/i,
        loader: "babel-loader",
      },
    ],
  },
};

module.exports = [clientConfig, serverConfig];

サーバー構成では、targetプロパティを にnode、entryポイントを にsrc/index.js、そして をoutputに設定しますserver.bundle.js。

package.jsonコードを構築して実行するためのスクリプトも追加します。

// package.json

{
  // ...
  "scripts": {
    "start": "yarn build:dev && node build/server.bundle.js",
    "build": "webpack --mode=production --node-env=production",
    "build:dev": "webpack --mode=development",
    "build:prod": "webpack --mode=production --node-env=production"
  }
}

「ネットワーク」タブを調べると、サーバーが生成した HTML が表示されますlocalhost:3000。<script src=”client.bundle.js” async=””></script>要素タブでは、タグの前にタグが挿入されているのが確認できるはずです</body>。

サスペンスサポート

アプリにコンテンツを追加してみましょう。サイドバー、ブログ ページ、および「関連ブログ」コンポーネントが含まれます。コードを整理するために、App.jsx個別のコンポーネントにリファクタリングします。

また、HTML マークアップを というコンポーネントにリファクタリングしますHtml。これは、Appコンポーネントが次のようになることを意味します。

import React, { lazy, Suspense } from "react";
import { Html } from "./Html.jsx";
import { Loading } from "./components/Loading.jsx";

const Sidebar = lazy(() =>
  import("./components/Sidebar.jsx" /* webpackPrefetch: true */)
);
const Main = lazy(() =>
  import("./components/Main.jsx" /* webpackPrefetch: true */)
);
const Extra = lazy(() =>
  import("./components/Extra.jsx" /* webpackPrefetch: true */)
);

export const App = () => {
  return (
    <Html>
      <Suspense fallback={<Loading />}>
        <Sidebar></Sidebar>
        <Suspense fallback={<Loading />}>
          <Main></Main>
          <Suspense fallback={<Loading />}>
            <Extra></Extra>
          </Suspense>
        </Suspense>
      </Suspense>
    </Html>
  );
};

  • Sidebar、、、MainおよびはExtra標準の React コンポーネントであるため、ここではコードの詳細については説明しません。
  • lazy: これはコンポーネントを遅延ロードするために使用されます。これは、レンダリングが必要な場合にのみインポートされることを意味します。その結果、Webpack でコードが分割され、buildフォルダー内のコンポーネントごとに個別のバンドル ファイルが作成されます。client.bundle.jsこれらのファイルは、バンドルがブラウザにロードされた後にロードされます。
  • Suspense: これは、データのフェッチとレンダリングを同時に処理する React コンポーネントです。コンポーネントがオンデマンドでロードされるのを待機し、準備が整うまでlazyロード インジケーター (プロップを使用) を表示します。fallback

たとえば、ルートまでの HTML がdiv最初に送信され、次にSidebarコンポーネント、次にMainコンポーネント、最後にExtraコンポーネントが送信されます。

これにより、ユーザーはすべてのコンポーネントが一度に読み込まれるまで待つ必要がなくなるため、ユーザー エクスペリエンスが向上します。コンポーネントは利用可能になると徐々に表示されます。

これを実際に確認するには、Appコンポーネントへのコンポーネントの読み込みを人為的に遅らせることができます。

const Sidebar = lazy(
  () =>
    new Promise((resolve) => {
      setTimeout(
        () =>
          resolve(
            import("./components/Sidebar.jsx" /* webpackPrefetch: true */)
          ),
        1000
      );
    })
);
const Main = lazy(
  () =>
    new Promise((resolve) => {
      setTimeout(
        () =>
          resolve(import("./components/Main.jsx" /* webpackPrefetch: true */)),
        2000
      );
    })
);
const Extra = lazy(
  () =>
    new Promise((resolve) => {
      setTimeout(
        () =>
          resolve(import("./components/Extra.jsx" /* webpackPrefetch: true */)),
        3000
      );
    })
);

さらに、メソッドのオプションを使用して HTML ストリーミングを制御できますrenderToPipableStream。

最初のレンダリング時ではなく、ページ全体が読み込まれたときに、すべての HTML を一度にストリーミングする場合は、オプションのonAllReady代わりに使用できます。onShellReady

const stream = renderToPipeableStream(<App />, {
  bootstrapScripts: ["client.bundle.js"],
  onAllReady() {
    response.setHeader("content-type", "text/html");
    stream.pipe(response);
  },
});

const stream = renderToPipeableStream(<App />, {
  bootstrapScripts: ["client.bundle.js"],
  onShellReady() {
    response.setHeader("content-type", "text/html");
    stream.pipe(response);
  },
});

setTimeout(() => {
  stream.abort()
}, 10000)

以上です、皆さん。この記事が、React コンポーネントのサーバー側レンダリングの複雑さを理解するのに役立つことを願っています。読んでくれてありがとう!

役立つリンク

  • https://github.com/rajgaur98/react-ssr/tree/main
  • https://github.com/reactwg/react-18/discussions/37

無料の週刊ニュースレターにご登録ください。Twitter、LinkedIn、YouTube、Discordでフォローしてください。

Circuitを使用して、テクノロジー関連スタートアップの認知度を高め、採用を促進します。