React 18では、アプリケーションのレンダリング方法が根本的に変わる「concurrent(同時)」な機能が導入されました。これらの最新機能がアプリケーションのパフォーマンスにどのような影響を与えるかを探ってみましょう。
まずは、長いタスクと対応するパフォーマンスの測定について基本を理解しましょう。
メインスレッドと長いタスク
ブラウザでJavaScriptを実行する際に、JavaScriptエンジンは「メインスレッド」と呼ばれるシングルスレッドの環境でコードを実行します。JavaScriptコードの実行だけでなく、メインスレッドはクリックやキーストロークなどのユーザーの操作、ネットワークイベントの処理、タイマーの実行、アニメーションの更新、ブラウザの再レイアウトや再描画の管理など、他のタスクも処理します。
メインスレッドはタスクを一つずつ処理します
メインスレッドはタスクを一つずつ処理します
タスクが処理されている間は、他のすべてのタスクは待機する必要があります。小さなタスクはブラウザによってスムーズに実行され、シームレスなユーザーエクスペリエンスを提供しますが、長いタスクは他のタスクの処理をブロックする可能性があるため問題となります。
50ミリ秒以上かかるタスクは「長いタスク」と見なされます。
長いタスクには赤い角が付けられます。合計ブロッキング時間は4425.40msです。
これは、デバイスが滑らかな視覚エクスペリエンスを維持するために、16ms(60fps)ごとに新しいフレームを作成する必要があることに基づいています。しかし、デバイスはユーザーの入力に対応したりJavaScriptを実行したりする必要もあります。
50ミリ秒の基準は、デバイスがフレームのレンダリングと他のタスクのリ
ソースを割り当てるために必要な時間を提供し、滑らかな視覚エクスペリエンスを維持するために約33.33ミリ秒の余裕を持つために設定されています。RAILモデルに関するこのブログ記事で50ミリ秒の基準について詳細に読むことができます。
最適なパフォーマンスを維持するためには、長いタスクの数を最小限に抑えることが重要です。ウェブサイトのパフォーマンスを測定するために、長いタスクがパフォーマンスに与える影響を測定する2つの指標があります:Total Blocking Time(TBT)とInteraction to Next Paint(INP)。
Total Blocking Time(TBT)は、First Contentful Paint(FCP)とTime to Interactive(TTI)の間の時間を測定する重要な指標です。TBTは、50msを超えるタスクの実行時間の合計を表し、ユーザーエクスペリエンスに重大な影響を与える可能性があります。
TBTは45msです。TTIよりも前に50msを超えた2つのタスクがあり、それぞれ30msと15msを超えています。合計ブロッキング時間はこれらの値の合計です:30ms + 15ms = 45ms。
Interaction to Next Paint(INP)は、ユーザーの最初のインタラクション(例:ボタンのクリック)から次の画面描画までの時間を測定する新しいCore Web Vitalsの指標です。この指標は、eコマースサイトやソーシャルメディアプラットフォームなど、多くのユーザーインタラクションがあるページに特に重要です。この指標は、ユーザーの現在の訪問中におけるすべてのINPの測定を蓄積し、最悪のスコアを返します。
Interaction to Next Paintは250msです。これが最も長い視覚的な遅延です。
このように、React 18の新しいアップデートはこれらの測定を最適化し、ユーザーエクスペリエンスを改善する方法に影響を与えています。まずは、従来のReactの動作を理解することから始めましょう。
従来のReactのレンダリング
Reactにおける視覚的なアップデートは、レンダーフェーズとコミットフェーズの2つのフェーズに分かれています。Reactのレンダーフェーズは純粋な計算フェーズであり、React要素が既存のDOMと調整(比較)されます。このフェーズでは、新しいReact要素の木構造、つまり「仮想DOM」と呼ばれるものが作成されます。これは実際のDOMの軽量のインメモリ表現です。
レンダーフェーズでは、Reactは現在のDOMと新しいReactコンポーネントツリーの違いを計算し、必要な更新を準備します。
![Reactは非同期レンダリングによってコンポーネントツリーのレンダリングを一時停止して、他の重要なタスクに優先順位を付けることができます](https://vercel.com/_next/image?url=https%3A%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F5hpQJpYBiWcSZJgCmIVhqn%2F9bf89a0119d431224a63755a6966409e%2FGroup_513856.png&w=3840&q=75&dpl=dpl_8h58
ia4wT2Aem5qZ1wNnrSbG4cFG)
Reactは非同期レンダリングによってコンポーネントツリーのレンダリングを一時停止して、他の重要なタスクに優先順位を付けることができます
レンダーフェーズの計算が完了すると、Reactは新しい仮想DOMツリーと現在のDOMツリーを比較し、どの部分が更新されるかを特定します。そして、コミットフェーズで更新が実際にDOMに適用されます。
このフェーズは同期的に実行され、DOM更新は一度に行われます。しかし、この同期的なコミットにより、長いレンダリングタスクが他のタスクの実行を妨げることがあります。
React 18の変更:同時モード
React 18では、「concurrent(同時)」モードが導入されました。このモードでは、Reactのレンダリングが非同期に行われるため、長いレンダリングタスクが他のタスクをブロックすることがなくなります。これにより、アプリケーションのパフォーマンスが向上します。
同時モードでは、Reactはタスクの優先順位を付けて処理します。すべてのタスクが同じ優先順位を持っているわけではありません。例えば、ユーザーの操作に対応するタスクはより高い優先順位を持ちます。これにより、滑らかなユーザーエクスペリエンスを提供することができます。
同時モードのもう一つの利点は、エラーのバウンダリの設定です。従来のReactでは、エラーがある部分の近くで他のコンポーネントのレンダリングが中止されることがあります。しかし、同時モードでは、エラーがある部分だけでなく他のコンポーネントも継続してレンダリングされるため、ユーザーが不完全なページを見ることがなくなります。
同時モードは、Reactのアプリケーションのパフォーマンスを向上させるための重要な機能です。従来のReactのレンダリングモデルでは、長いタスクが他のタスクをブロックし、パフォーマンスが低下することがありましたが、同時モードを使用することでこれを回避することができます。
同時モードの有効化
React 18の同時モードを有効にするためには、アプリケーションのルートコンポーネントで次のコードを追加します。
import React from 'react';
const App = () => {
return (
<React.StrictMode>
<React.unstable_ConcurrentMode>
{/* アプリケーションのコンポーネント */}
</React.unstable_ConcurrentMode>
</React.StrictMode>
);
};
export default App;
このコードは、アプリケーション全体をStrictModeでラップし、ConcurrentModeを有効にしています。StrictModeは開発モードでのみ使用され、アプリケーション内の潜在的な問題を検出して警告します。ConcurrentModeはStrictMode内に入れる必要があります。こうすることで、React 18の新機能を使用することができます。
同時モードは、非同期レンダリングを実現するための新しいアプローチであり、アプリケーションのパフォーマンスを向上させるための重要な機能です。これにより、長いタスクが他のタスクをブロックすることなく、滑らかなユーザーエクスペリエンスを提供することができます。
トランジション
新しい**useTransition**
フックで利用できる**startTransition
**関数を使用することで、更新を緊急でないとマークすることができます。これは、同期的にレンダリングされるとユーザーエクスペリエンスに悪影響を与える可能性のある視覚的な変更を示す「トランジション」として特定の状態更新をマークできる強力な新機能です。
**startTransition
**で状態更新をラップすることで、現在のユーザーインターフェースを優先して保持するために、レンダリングを遅延または中断することができます。
import { useTransition } from "react";
const [isPending, startTransition] = useTransition();
トランジションが開始すると、並行レンダラーが新しいツリーをバックグラウンドで準備します。レンダリングが完了すると、ReactスケジューラーがDOMを新しい状態に更新するためにパフォーマンスの良い方法を探し続けます。これは、ブラウザーがアイドル状態になったときや、ユーザーのインタラクションなどのより高い優先度のタスクが保留中でない場合に行われるかもしれません。
**CitiesList**
デモでは、**setCities
を各キーストロークごとに直接呼び出す代わりに、状態更新をstartTransition
**でラップすることが理想的です。これにより、状態更新がユーザーに視覚的な変更をもたらす可能性があり、ユーザーエクスペリエンスに悪影響を与える可能性があるため、Reactは更新を即座にコミットせずにバックグラウンドで新しい状態を準備するように指示されます。
import React, { useState } from "react";
import CityList from "./CityList";
export default function SearchCities() {
const [text, setText] = useState("Am");
return (
<main>
<h1><code>startTransition</code></h1>
<input type="text" onChange={(e) => setText(e.target.value) } />
<CityList searchQuery={text} />
</main>
);
};
コード編集モードに入るには、Enterキーを押します。編集モードを終了するには、Escapeキーを押します。
コードを編集中です。編集モードを終了するには、Escapeキーを押します。
今、入力フィールドに文字を入力すると、キーストローク間に視覚的な遅延がなくなり、ユーザーインターフェースが滑らかに保持されます。これは、**text**
状態が依然として同期的に更新されるためであり、これは入力フィールドが**value**
として使用する値です。ただし、**CitiesList**
コンポーネントは、状態更新を**startTransition
でラップしています。
バックグラウンドでは、Reactは各キーストロークごとに新しいツリーをレンダリングし始めます。ただし、これが完全な同期的なタスクである必要はありません。Reactは、現在のUI(「古い」状態を表示する)が引き続きユーザーの入力に対応できるように、新しいバージョンのコンポーネントツリーをメモリ内に準備し始めます。
パフォーマンスタブを見ると、状態更新をstartTransition
**でラップすることで、長時間タスクの数と合計ブロック時間が、トランジションを使用しない実装のパフォーマンスグラフと比較して大幅に減少したことがわかります。
![The performance tab shows that the number of long tasks and the total blocking time have reduced significantly.](https://vercel.com/_next/image?url=https%3A%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2FM1dFGbcX3mxerAn1U6Bbi%2F8525d99eb629e72c61ff
90459f57523c%2FScreenshot_2023-07-05_at_2.20.04_PM.png&w=3840&q=75&dpl=dpl_8h58ia4wT2Aem5qZ1wNnrSbG4cFG)
パフォーマンスタブを見ると、長時間タスクの数と合計ブロック時間が大幅に減少していることがわかります。
パフォーマンスタブを見ると、長時間タスクの数と合計ブロック時間が大幅に減少していることがわかります。
トランジションは、Reactのレンダリングモデルの根本的な変更の一部であり、Reactに複数のバージョンのUIを同時にレンダリングし、異なるタスクの優先度を管理する能力をもたらします。これにより、高頻度の更新やCPU集中型のレンダリングタスクを扱う際に特に滑らかで応答性のあるユーザーエクスペリエンスが可能となります。
React Server Components
React Server ComponentsはReact 18の実験的な機能ですが、フレームワークが採用するための準備が整っています。これは、Next.jsについて掘り下げる前に知っておくと重要です。
従来、Reactはアプリをレンダリングするいくつかの主要な方法を提供していました。アプリを完全にクライアント側でレンダリングすることもできます(クライアントサイドレンダリング)、またはコンポーネントツリーをサーバー上でHTMLとしてレンダリングし、この静的なHTMLをJavaScriptバンドルとともにクライアントに送信してコンポーネントをクライアント側で復元する方法もあります(サーバーサイドレンダリング)。
いずれの方法も、同期的なReactレンダラーがクライアント側でコンポーネントツリーを再構築する必要があるため、サーバー上で既に利用可能なこのコンポーネントツリーをJavaScriptバンドルを使ってクライアントに再度レンダリングする必要があります。
React Server Componentsを使用すると、Reactは実際のシリアル化されたコンポーネントツリーをクライアントに送信できます。クライアント側のReactレンダラーはこの形式を理解し、静的なHTMLファイルまたはJavaScriptバンドルを送信する必要なく、効率的にReactコンポーネントツリーを再構築します。
この新しいレンダリングパターンを使用するには、**react-dom/server
のrenderToPipeableStream
メソッドとreact-dom/client
のcreateRoot
**メソッドを組み合わせます。
import App from '../src/App.js'
app.get('/rsc', async function(req, res) {
const {pipe} = renderToPipeableStream(React.createElement(App));
import { createRoot } from 'react-dom/client';
import { createFromFetch } from 'react-server-dom-webpack/client';
export function Index() {
return createFromFetch(fetch('/rsc'));
const root = createRoot(document.getElementById('root'));
⚠️ これは、以下に示すCodeSandboxデモの単純化された(!)例です。
完全なCodeSandboxデモを見るにはここをクリックしてください。次のセクションでは、より詳細な例について説明します。
デフォルトでは、ReactはReact Server Componentsをハイドレーションしません。コンポーネントは、**window
オブジェクトへのアクセスやuseState
やuseEffect
のようなフックの使用など、クライアントサイドの対話性を利用しません。
コンポーネントとそのインポートをクライアントに送信するためにJavaScriptバンドルに追加するには、「use client」バンドラーディレクティブをファイルの先頭に記述します。これにより、バンドラーはこのコンポーネントとそのインポート**をクライアントバンドルに追加し、Reactにツリーをクライアント側でハイドレートするよう指示します。このようなコンポーネントはClient Componentsと呼ばれます。
注意: フレームワークの実装は異なる場合があります。たとえば、Next.jsでは、クライアントコンポーネントをサーバー上でHTMLにプリレンダリングする従来のSSRアプローチに似た方法で行いますが、デフォルトではクライアントコンポーネントはCSRアプローチと同様にレンダリングされます。
注意: フレームワークの実装は異なる場合があります。たとえば、Next.jsでは、クライアントコンポーネントをサーバー上でHTMLにプリレンダリングする従来のSSRアプローチに似た方法で行いますが、デフォルトではクライアントコンポーネントはCSRアプローチと同様にレンダリングされます。
開発者は、クライアントコンポーネントを使用する際にバンドルサイズを最適化する必要があります。これは次のように行えます。
- インタラクティブなコンポーネントの最も葉側のノードだけが**
"use client"
**ディレクティブを定義するようにすること。これにはいくつかのコンポーネントの切り離しが必要になるかもしれません。 - 直接インポートする代わりに、コンポーネントツリーをpropsとして渡すこと。これにより、Reactが**
children
**をReact Server Componentsとしてレンダリングし、それらをクライアントバンドルに追加することなくレンダリングできます。
Suspense
もう1つの重要な新しいConcurrentの機能は、Suspenseです。これはReact 16で**React.lazy
と共にコード分割のためにリリースされたものとは全く同じものですが、React 18で導入された新機能により、Suspense
**がデータの取得にも拡張されました。
**Suspense
**を使用すると、データがリモートソースから読み込まれるまでコンポーネントのレンダリングを遅延させることができます。その間、まだロード中であることを示すフォールバックコンポーネントをレンダリングすることができます。
宣言的にロード状態を定義することで、条件付きのレンダ
リングと非同期データの読み込みを同時に処理できるようになります。
次の例では、**useEffect
**フックを使用して非同期でデータを取得しますが、データが読み込まれるまでフォールバックコンポーネントをレンダリングします。
import React, { useState, useEffect } from 'react';
const DataComponent = () => {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(response => response.json())
.then(data => setData(data))
}, []);
if (!data) {
return <FallbackComponent />;
}
return (
<React.Fragment>
{data.map((item) => (
<p key={item.id}>{item.text}</p>
))}
</React.Fragment>
);
};
const FallbackComponent = () => {
return <p>Loading data...</p>;
};
export default DataComponent;
フォールバックコンポーネントを表示する前に、ローディング状態が表示されるため、ユーザーエクスペリエンスが向上します。この例では、**DataComponent
がデータを取得する間にフォールバックコンポーネントを表示しますが、他のデータ読み込み方法を使用することもできます(たとえば、useTransition
を使用して遅延を実現したり、React.SuspenseList
**を使用して複数のローディング状態を定義することができます)。useSWR
などのサードパーティのデータフェッチングライブラリを使用することもできます。
結論
React 18による同時モードの導入により、アプリケーションのパフォーマンスが大幅に向上しました。同時モードは、非同期レンダリングにより長いタスクのブロックを回避し、ユーザーエクスペリエンスを改善するための重要な機能です。アプリケーションのルートコンポーネントでConcurrentModeを有効にすることで、React 18の新機能を利用することができます。
今後のバージョンでも引き続き、Reactチームはアプリケーションのパフォーマンスを向上させるための新機能を導入していく予定です。React 18にアップグレードすることで、アプリケーションのレンダリング性能を最適化し、ユーザーエクスペリエンスを向上させることができます。