React Rendering
Overview
Reactにおいてのレンダリングについてまとめたセクション。
このセクションでは以下についてまとめている。
- CSR(Client-Side Rendering)
- SSR(Server-Side Rendering)
- SSG(Static-Site Generation)
- RSC(React Server Components)
Next.jsと混同しがちだが、これらの概念はNext.jsに限らずReactにも存在する点に注意が必要。
Next.jsは、Reactが提供するこれらの機能を使いやすく拡張したフレームワーク。
レンダリング前に意識すべきポイント
Webページのパフォーマンス指標として、以下の要素が挙げられる。各レンダリング手法にはそれぞれの課題があるが、これらは徐々に解決されてきた。
FCP(First Contentful Paint)
ブラウザが最初のテキストや画像などのコンテンツを描画するまでの時間
真っ白な画面ではなく、何かしらの表示(レイアウトなど)がある状態。
LCP(Largest Contentful Paint)
ページの主要なコンテンツが表示されるまでの時間
DBからデータを取得し、UIにレンダリングされた状態。
TTI(Time To Interactive)
ページが完全にインタラクティブになるまでの時間
Reactがダウンロードされ、アプリケーションがレンダリングされ、ハイドレーションが行われる
ページ上のUIコンポーネントが反応し始め、ユーザーが入力できる状態
CSR (Client-Side Rendering)
ReactではCSR戦略が用いられてきた。
CSRでは、ブラウザが初期に最小限のHTMLを受け取った後、JavaScriptがブラウザ上で実行されて、ページのコンテンツがレンダリングされる。
Reactを使った通常のSPA(Single Page Application)がCSRの典型であり従来のReactアプリケーションで使われている方法。
最初にサーバーからHTMLが送信され、その後JavaScriptがクライアント側で実行され、ページが完全に表示される。
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
CSRでは、クライアントが受け取るのは次のような中身のない空の HTML
ファイル。
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="/static/js/bundle.js"></script>
</body>
</html>
bundle.js
にはReactアプリケーションを構築するために必要なものがすべて含まれている。
JSがダウンロードされて解析が完了すると、Reactが動作し始め、アプリケーション全体のすべてのDOMノードを呼び出し、それを空の <div id="root">
に格納する。
CSRではFCPが遅くなってしまうのが問題。
そしてこの問題を解決するのがSSR。
デメリット
- CSRではFCPが遅くなってしまうのが問題。それを解決するのがSSR。
- ページロードが遅れる、インタラクションは非常にスムーズ。
SSR (Server-Side Rendering)
SSRでは、サーバーがユーザーに完全にレンダリングされたHTMLを返す。
これにより、ユーザーはページが初めて読み込まれる際にすぐにコンテンツを見ることができる。
JavaScriptはクライアント側でさらに動作し、インタラクティブな要素を有効にする。
Reactにおける例
React自体にはSSR機能はないが、Express
などのサーバーでReactコンポーネントをレンダリングし、その結果をブラウザに返すことができる。
ReactDOMServer.renderToString()
というメソッドを利用することで、React Componentをサーバー上で HTML
として扱うことができ、hydrateRoot
を利用することでその HTML
に JavaScript
をアタッチしてインタラクティブな動作を実現できる。
// Node.jsやExpressでのSSR
import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import App from './App';
const server = express();
server.get('*', (req, res) => {
const content = ReactDOMServer.renderToString(<App />);
res.send(`
<html>
<body>
<div id="root">${content}</div>
<script src="/bundle.js"></script>
</body>
</html>
`);
});
server.listen(3000);
ハイドレーションとは、バンドルされたJSをクライアント側で実行し以下2つを行う処理のこと。
- インタラクティブ性の追加
サーバー側で生成されたHTMLに対し、イベントハンドラーやその他のスクリプトを結び付ける。
これにより、静的なページがインタラクティブなアプリケーションとして機能するようになる。 - 仮想DOMの生成と実DOMの同期
バンドルされるJSはページのインタラクティブ性を担うものだけではない。
サーバー側で生成されたHTMLを基に構築されるDOMをクライアント側でも構築するためのコードも含まれている。
クライアント側はJSで仮想DOMを構築し、サーバー側で生成されたHTMLを基に構築された実DOMと比較する。
比較によりサーバー側でレンダリングされたコンテンツと、クライアント側でレンダリングされたコンテンツが同一であるかを確認し、同一でなければクライアント側でDOMの再構築が行われる。
メリット
SSRの大きな利点は、FCPだけでなくLCPの改善も可能であること。
しかし、必要なデータをクライアント側で取得している状態だと、LCPは改善されない。
ユーザーはローディング画面を見るためにサイトへアクセスしているのではない。
ユーザーが望むのは、DBから取得した情報が表示されているUI。
Next.jsをはじめとするフレームワークでは、この問題を解決するためにサーバー側でのデータ取得を簡単に可能にしている。
SSRのデメリット
SSRにより、CSRが抱える問題を改善できましたが、まだ問題は残っている。
- ページ単位という制限 SSRでは、サーバー側でのデータ取得とレンダリングがページ単位でしか機能しない。 そのためデータ取得などが原因でサーバー側の処理が重くなると、プリレンダリングに時間がかかる。 SSRではプリレンダリングが完了するまでブラウザには何も表示されないため、FCPが遅くなる可能性。
- SSR手法が標準化されていない SSRはNext.js、Gatsby、Remixなどのフレームワークがそれぞれ独自の方法を採用している。 この非標準化は、技術選択や移行に際しての複雑性を増大させる。
- 常にクライアント上でハイドレーションを行う SSRを使用しても、最終的にはクライアント上でJSによるハイドレーションが必要。 全てのコンポーネントは、たとえ不要な場合でも実DOMとの比較のために仮想DOMの構築処理が行われる。
ページ単位で機能させるということは、必然と props
によるバケツリレーが多くなる。
これらの問題を解決するために生み出されたのが React Server Components
SSG (Static-Site Generation)
CDNに静的ファイルをキャッシュすることで表示のスピードUPを実現できる。
- 概念: SSGは、ページがビルド時に静的HTMLファイルとして生成され、その後のリクエストではサーバーがこれらの静的ファイルを返すという方法。ビルド時にページが作成されるため、ページのパフォーマンスが向上する。
- Reactにおける例: React単体ではこの機能は提供されていませんが、Next.jsはSSG機能を提供している。
getStaticProps
やgetStaticPaths
を使って、ビルド時にコンテンツを静的に生成できる。
// Next.jsにおけるSSGの例
export async function getStaticProps() {
const res = await fetch('https://api.example.com/data');
const data = await res.json();
return {
props: { data }, // ページコンポーネントにデータを渡す
};
}
export default function Page({ data }) {
return <div>{data.content}</div>;
}
Next.jsでの統合
Next.jsはこれらのレンダリング手法を統合的にサポートしており、アプリケーションのニーズに合わせて選択できる。
- CSR: Next.jsでもCSRは可能ですが、主に
useEffect
などのクライアントサイド専用機能で行われる。 - SSR:
getServerSideProps
を使うことでサーバーサイドレンダリングが簡単に実現できる。 - SSG:
getStaticProps
やgetStaticPaths
を使用して、静的生成をする。
React自体とNext.jsの違い
- React単体: デフォルトではCSRでレンダリングされる。SSRやSSGを使うには追加の設定やフレームワークが必要。
- Next.js: Reactの上に構築されており、CSR、SSR、SSGのすべてを選択的にサポートしている。
React Server Components(RSC)
React Server Component(RSC)とは、Reactコンポーネントのレンダリングプロセスにおけるアーキテクチャで、CSRやSSRといったレンダリング手法の問題点を解決するために生まれた。
SC
とは、サーバー側でのみ実行されるコンポーネント。
サーバー側でのみ実行されるので、JSバンドルには含まれない。
そのため SSR
の問題点であった「常にクライアント上でハイドレーションを行う」を解決できる。
React Server ComponentsはHTMLをサーバ側で生成する従来のSSRとは根本的に異なる。 サーバ側では仮想DOMの生成までを行う。
サーバコンポーネントのレンダリングの結果(仮想DOM)はHTTPリクエストを介してブラウザに渡り、ブラウザ側でクライアントコンポーネントと合わせてレンダリングを完成させる。
React Server Componentsでは次の3種類のコンポーネントが登場する。
サーバコンポーネント
- サーバ(Node)でのみレンダリングされるコンポーネント。
- ファイル名の末尾が
.server.js
→.server.tsx
に変更可能。 - このコンポーネントで使用するコードはブラウザがダウンロードするJSにはバンドルされない。
- サイズの大きいライブラリも使いやすい。
- DBなどのサーバリソースにアクセス可能。
- 状態を持てず、イベントのハンドリングができない(要はwindowオブジェクトにアクセスできない)
クライアントコンポーネント
ブラウザでのみレンダリングされるコンポーネント。ファイル名の末尾が .client.js
- 状態が持てる
- ブラウザAPIにアクセス可能
- イベントハンドルが可能
ユニバーサルコンポ ーネント
インポート先に応じて、両側で使用およびレンダリング可能なコンポーネント
サーバコンポーネントとクライアントコンポーネントの両方の制約(できないこと)を持つ。
- サーバコンポーネントから他のサーバコンポーネントやクライアントコンポーネントをインポートできる。
- クライアントコンポーネントからサーバコンポーネントをインポートできない。(コンポーネントの親子関係ではなく、ファイルのインポートの親子関係に関する制約)
背景(レンダリング技術の歴史)
WEBサイトのレンダリング技術はここ10年で、昔ながらのSSR(Server Side Rendering) からReactやVueを用いたSPA(Single Page Application) に移行した。
SPAは「UXの向上」や「ページ遷移の高速化」など利点があるが、「初期表示が遅い」「動的なOGP対応が困難」などの欠点もある。
その欠点を補うべく、NextやNuxtではSPAとSSRやSSG(Static Site Generator)を組み合わせる手法がとられるようになった。
React Server Componentsではこれまでとは別のアプローチで、SPAとSSRの良いとこ取りを目指す。