はじめに
業務でReact.jsやNext.jsを使用していますが、特にレンダリングに関する知識が整理できていなかったことや、React18の新しい機能や概念を今後の開発のために整理したいと考えて今回の記事を作成するに至りました。
React.jsのレンダリング
React は必要な箇所のみを更新する
React DOM は要素とその子要素を以前のものと比較し、DOM を望ましい状態へと変えるのに必要なだけの DOM の更新を行います。
React.jsは、JavaScriptのライブラリであり、仮想DOMを構築して実際のDOMとの差分を検知し必要な部分だけを更新することで高速なレンダリングを実現します。それにより、素早い動作をユーザーに提供することができています。一方で、SPAの場合では、Reactコンポーネント(JavaScriptファイル)はクライアントサイドで実行されるため、初回ロードに時間がかかったりします。また、クライアントサイドでReactコンポーネントが実行されることで、画面が描画されるため、Googleのクローラには検知されにくいというデメリットもあるようです。
Next.jsのレンダリング
そんなReactのデメリットを解決するために登場したのが、ReactのフレームワークであるNext.jsです。Next.jsには他にも様々な機能を持っていますが、今回はレンダリングに焦点を当てて見ていきます。
pre-rendering
By default, Next.js pre-renders every page. This means that Next.js generates HTML for each page in advance, instead of having it all done by client-side JavaScript. Pre-rendering can result in better performance and SEO.
Next.jsでは、各ページのHTMLをクライアントサイドのJavaScriptで全て生成するのではなく事前に生成してくれます。この機能をpre-rendering(プリレンダリング)と呼びます。プリレンダリングは、従来のReactのデメリットを補うように、ブラウザへの初期表示速度の向上や、HTMLを描画したページを初期表示するためにSEOの向上に貢献します。
そして、プリレンダリングには2種類の方法と3種類のパターンがあると考えられます。
SSGとSSR
SSG
Static Generation (Recommended): The HTML is generated at build time and will be reused on each request.
SSGは、Static Site Generation の頭文字をとった造語で、静的生成と呼びます。プリレンダリングのタイミングはビルド時(next build)です。
メリットは、特別な設定をしなくてもCDNにキャッシュされ、リクエスト毎にページをレンダリングするよりも非常に高速になることです。そのため、Next.jsではSSGが公式に推奨されています。
SSR
Server-side Rendering is the pre-rendering method that generates the HTML on each request.
SSRは、Server Side Renderingの頭文字をとった造語です。プリレンダリングのタイミングは、ユーザーのリクエスト毎です。
メリットは、速度は低下する一方で常に最新の状態でプリレンダリングされたページを提供することができることです。
プリレンダリングのパターン
getStaticPropsを使用する
getStaticPropsは、ビルド時に外部データを取得して、それをpropsとして各ページに利用可能にするための非同期関数です。SSGでは、ビルド時にデータを取得してページを生成することから、データもビルド時の状態が保持されるということですね。
export default function Home(props) { ... }
export async function getStaticProps() {
// 外部からデータを取得する
const data = ...
// propsプロパティの値はHomeコンポーネントのpropsに渡される
return {
props: ...
}
}
getServerSidePropsを使用する
getServerSidePropsは、リクエスト時に呼び出されて、データを取得しそれをpropsとして各ページに利用可能にするための非同期関数です。SSRでは、リクエスト時にデータを取得してページを生成することから、データもリクエスト時の最新の状態であるということですね。
export async function getServerSideProps(context) {
return {
props: {
// props for your component
},
};
}
getStaticPropsもgetServerSidePropsもどちらも使用しない
Next.js automatically determines that a page is static (can be prerendered) if it has no blocking data requirements. This determination is made by the absence of getServerSideProps and getInitialProps in the page.
getServerStaticPropsもgetInitialProps(旧来のSSRでのデータ取得の方法で、現在はgetServerSidePropsの使用が推奨されている)も使用しない場合は、静的生成可能と見なされて、静的生成つまりSSGでプリレンダリングされることになります。このことからもSSGを推奨していることが分かると思います。
Page Size First Load JS
┌ ● / 1.52 kB 84.7 kB
├ /_app 0 B 71.5 kB
├ ○ /404 194 B 71.7 kB
├ λ /api/hello 0 B 71.5 kB
├ λ /api/post/[pid] 0 B 71.5 kB
├ ○ /default 445 B 71.9 kB
├ └ css/68dbcda016eb10d2.css 206 B
└ ● /posts/[id] (398 ms) 1.32 kB 84.5 kB
├ /posts/pre-rendering
└ /posts/ssg-ssr
+ First Load JS shared by all 71.5 kB
├ chunks/framework-91d7f78b5b4003c8.js 42 kB
├ chunks/main-eab312c0bf2a7270.js 28.2 kB
├ chunks/pages/_app-73483fad2904193b.js 508 B
├ chunks/webpack-514908bffb652963.js 770 B
└ css/ccfdf9d4db962a53.css 272 B
λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps)
○ (Static) automatically rendered as static HTML (uses no initial props)
● (SSG) automatically generated as static HTML + JSON (uses getStaticProps)
こちらは、next buildを使用してビルドした時の挙動です。このように、SSGは○
、SSRはλ
、それ以外は●
で表現されて、ビルド時に各ファイルがどのようにビルドされているのか直感的に分かるようになっています。
番外編 ISR
Next.js allows you to create or update static pages after you’ve built your site. Incremental Static Regeneration (ISR) enables you to use static-generation on a per-page basis, without needing to rebuild the entire site. With ISR, you can retain the benefits of static while scaling to millions of pages.
ISRは、Incremental Static Regenerationの頭文字をとった造語です。ISRは、SSGをベースにした機能で、ビルド時にプリレンダリングされたページをサイト全体を再構築することなくページ単位で静的生成を行います。具体的には下記のようにgetStaticProps関数の戻り値にrevalidateプロパティを追加します。ここで指定された秒数経過後にリクエストを受けた際にページの静的生成を開始します。
function Blog({ posts }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
export async function getStaticProps() {
const res = await fetch('https://.../posts')
const posts = await res.json()
return {
props: {
posts,
},
// 10秒経過後にリクエストを受けることで再生成される
revalidate: 10,
}
}
上記のコードは、公式で以下のような説明が示されています。
When a request is made to a page that was pre-rendered at build time, it will initially show the cached page.
Any requests to the page after the initial request and before 10 seconds are also cached and instantaneous.
After the 10-second window, the next request will still show the cached (stale) page
Next.js triggers a regeneration of the page in the background.
Once the page generates successfully, Next.js will invalidate the cache and show the updated page. If the background regeneration fails, the old page would still be unaltered.
- ビルド時にプリレンダリングされたページへのリクエストがあった場合、最初はキャッシュされたページが表示される
- 最初のリクエスト以降、10秒前までのページへのリクエストもキャッシュされ、瞬時に終了する
- 10秒経過後の初めのリクエストではキャッシュされた(古い)ページが表示される
- Next.jsは、バックグラウンドでページの再生成を始める
- ページが正常に生成されると、Next.jsはキャッシュを無効にして、更新されたページを表示する
- もしバックグラウンドの再生成に失敗した場合は、古いページは変更されないままとなる
React 18
2022年3月29日にReact 18 が正式リリースされて久しいですが、主にレンダリングに関連する2つの機能を見てみます。
React Server Component
React Server Component(RSC) は、開発段階ではありますが、非常に面白い機能です。RSCの開発には下記3つの目的があるとされています。
- Good user experience
- Cheap maintenance
- Fast performance
つまり、開発者にとってもユーザーにとっても快適な体験を提供することを目的にしています。
これらを満たすために今も研究が続けられています。例えば下記のようなことが可能です。
- バンドルサイズを小さくする
- サーバーサイドでデータを取得する
バンドルサイズについては、アプリケーションの構造にもよりますが、研究チームによると、約30%の削減に成功したとのことです。これは、クライアントサイドでの通信に要するコストやハイドレーションするコストを削減できるため、動作の向上に繋がると思われます。
データ取得については、従来は、各コンポーネントは、クライアントサイドでデータを取得してレンダリングされてから、その子のコンポーネントがレンダリングされるという形でした。これを公式には、ネットワークウォーターフォールと呼んでいます。サーバーサイドでデータを取得することでこれを解消することができるため、素早い動作に繋がると思われます。
どちらをとっても、ユーザーと開発者にとってより速さのある体験を得られるようになるのが、RSCの最大の利点だと思われます。
RSCの開発により、Reactコンポーネントは以下の3種類になっています。
- Crient Component:クライアントサイドで実行可能
- Server Component:サーバーサイドで実行可能
- Shared Component:クライアントサイド、サーバーサイドの両方で実行可能
transition
transitionは、ユーザーにとって、緊急性の高い更新と高くない更新を区別するための機能です。
緊急性の高い更新とは、キーボード入力やクリックやプレスなどを指します。一方で、緊急性の高くない更新とは、緊急性の高い更新との相対的な位置付けです。公式では、下記の具体例が示されています。
例えば、ドロップダウン内でフィルタを選択した場合、フィルタボタン自体はクリックした瞬間に反応することを期待するでしょう。しかしフィルタの結果は、ボタンの反応とは別に徐々に現れても構いません。小さな遅延は認識できませんし、あって構わないものです。また、前のレンダーが終わっていない段階で再びフィルタを変更した場合、最終的な結果以外は気にしません。
また、transitionによる更新とは、遷移した画面の中で段階的にUIが変化するような更新のことを表します。
このような挙動を実現するために新たに導入されたのが、下記のフックです。
- useTransition: 保留状態を示す値とそれを開始するための関数を定義するためのフックです。
- startTransition: transitionによる更新を開始するためのメソッドです。
これらは例えば下記のように使用されます。
import {startTransition} from 'react';
// 緊急性の高い更新:キーボード入力した値が反映される
setInputValue(input);
// startTransitionの内部に対象のメソッドを記載する
startTransition(() => {
// 緊急性の高くない更新:入力した結果が反映される
setSearchQuery(input);
});
Suspence
サスペンス機能により、指定したコンポーネントが描画されるまでに、ロード中という状態を宣言的に記述できるようになりました。これは、streaming HTML と呼ばれる機能で、データ取得の前に静的に生成したHTMLを表示して、ハイドレーションまで終えたときに指定したコンポーネントを表示するといったことを実現します。また、指定したコンポーネントは非同期的に処理されることになるので、他のコンポーネントには影響はありません。
使用方法は、サスペンス機能を利用したいコンポーネントをSuspenceコンポーネントで囲って、fallbackにロード中に表示するコンポーネントを設定します。下記の例では、Commentsコンポーネントにおいて、データを取得してハイドレーションを終えるまで、Spinnerコンポーネントが表示されることになります。
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
また、Selective Hydration と呼ばれる機能もあり、Suspenceコンポーネントが複数ある場合には、ユーザーのクリックされた部分(コンポーネント)で優先してハイドレーションを行うことができます。
下記の例では、ユーザーがSidebarコンポーネント部分かCommentsコンポーネント部分をクリックすることで、優先してハイドレーションするコンポーネントを選択することができます。
<Wrapper>
<Suspense fallback={<Spinner />}>
<Sidebar />
</Suspense>
<Article />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</Wrapper>
ユースケース
SSG
更新頻度の低いページで、かつ、SEO対策を行いたい場合には、SSGが適していると言えます。公式には、下記のような場合を想定しています。
- マーケティングページ
- ブログ記事
- Eコマースの商品リスト
- ヘルプとドキュメント
SSR
更新頻度が比較的高く、リクエスト毎に更新する必要があって、SEO対策を行いたい場合には、SSRが適していると言えます。具体的には下記のような場合が考えられます。
- ニュース記事を掲載するページ
- ユーザーによる投稿を表示するページ
RSC
頻繁に情報が更新され、ユーザーの滞在時間が長いが、データの取得も行う必要があるような場合には、適していると考えられます。
- twitterのようなSNSのサービス
- youtubeのような動画配信サービス
transition
その画面の中で、ユーザーにとって緊急性の高い操作を優先して表示したい場合には適していると考えられます。その場合は、データの取得からレンダリングされるまでの時間がどれくらいかかるのか、または、時間がかかる可能性があるのか、を検証して推測する必要があると思われます。
Suspense
transitionと同様で、「データ取得→HTMLのレンダリング→JavaSciriptファイルの読込み→reactコンポーネントのハイドレーション」に時間がかかるようなケースがある場合や、ユーザーによって重要な機能である部分に対して利用することが適していると考えられます。
まとめ
これらはバラバラに存在する訳ではなく、かといって複雑に絡み合っている訳でもなく、各自がそれぞれの段階でそれぞれ機能するようになっています。React18については、今回導入された他の機能についても学びたいと思いますし、Next.jsもReact18に対応しているため、どちらの動向もチェックしていきたいと思います。
最後に、何かお気づきの点がありましたら、ご指摘を頂ければと思いますので、よろしくお願い致します。