並列レンダリングによるアプリのパフォーマンスの向上
並列レンダリングはReact 18の機能で、フレームワークが複数のUIバージョンを同時に準備できるようにします。Reactで並列レンダリングを使用すると、レンダリング操作を必要に応じて中断、一時停止、中止、または再開することができます。Reactでは、緊急性の高い更新かどうかを指定することで、メインスレッドをブロックせずに、複数の状態更新を一度に処理できます。緊急性の高いユーザー操作(入力、クリック、キーの押下)が優先的に処理され、緊急性の低い更新(大規模な画面の更新、データの取得)はバックグラウンドで処理されます。
並列レンダリングにより、Vegaアプリは次のことを行えます。
- スムーズな再生とインタラクションの維持: UIコントロールとメディアは、負荷の大きい更新中も応答し続けます。
- バックグラウンド処理の最適化: データの読み込みや計算によってインターフェイスがフリーズしません。
- ナビゲーションの向上: 複雑なレンダリングタスクの実行中でも、画面間の切り替えをスムーズに行えます。
Vegaアプリでの並列レンダリングの概要は、こちらのビデオをご覧ください。
フック、コンポーネント、ヘルパー関数
useTransitionとuseDeferredValueという2つのReactフックを使用すると、更新を管理したり、アプリのパフォーマンスを制御したりできます。不要な再レンダリングを防ぐため、これらのフックは、必ずコンポーネントのメモ化と組み合わせて使用してください。
Reactには、負荷の大きいコンポーネントのレンダリング中にフォールバックコンポーネントを表示するためのSuspenseコンポーネントも用意されています。lazyヘルパー関数を使用すると、負荷の大きいコンポーネントの読み込みを遅らせ、その後の更新に備えてキャッシュしておくことができます。
useTransition
useTransitionフックでは、状態更新を優先させて即座に処理するか、トランジション(優先度が低く、中断可能)として処理するかを指定できます。これにより、フィルタリング操作によってUIの更新が遅れるという、検索インターフェイスの一般的な課題が解決されます。デバウンスなどの手法を使用する代わりに、検索入力の応答性を維持しつつ、フィルタリングの優先度を低く設定できるようになりました。
このフックは、次の2つの値を返します。
isPending: 優先度の低い更新が保留中かどうかを表すブール値。読み込み状態を示す場合に役立ちますstartTransition: 緊急性が低い状態更新をラップする関数
例:
このサンプルコードでは、検索入力はsetQueryによって即座に更新されますが、フィルタリング操作はstartTransitionによって延期されます。トランジション中に読み込みインジケーターが表示され、準備ができた時点で結果が表示されます。
const SearchComponent = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleSearch = (query) => {
setQuery(query); // 検索入力を更新
startTransition(() => {
setResults(performSearch(query); // 検索操作を延期
});
};
return (
<View>
<TextInput
value={query}
onChange={handleSearch}
/>
{isPending && <LoadingIndicator />}
<SearchResults results={results} />
</View>
);
};
このコードが示すように、startTransitionは関数の実行を遅延させず、setResultsの更新を低優先度としてマークするだけです。Reactは、setQuery(高優先度のキュー)を最初に処理し、その後でsetResults(低優先度のキュー)を処理します。
このフックを直接使用できない場合は、startTransition関数を直接使用してください。
使用するタイミング: コンポーネントの状態変数を制御できる場合(検索インターフェイス、ナビゲーション、大規模なリストの更新)。
使用を避けるべきタイミング: コンポーネントの状態を直接制御できない場合(代わりにuseDeferredValueを使用します)
useDeferredValue
useDeferredValueフックを使用すると、重要な更新が完了するまで、プロパティや外部ソースの値の更新を遅らせることができます。たとえば、コンポーネントが検索結果を受け取ったとき、検索結果のプレビューを更新するという負荷の大きい処理を延期する一方で、見出しの検索語は即座に更新できます。
使用するタイミング: 状態変数にアクセスできない場合に、負荷の大きい計算や非同期操作をトリガーするプロパティ
使用を避けるべきタイミング: 状態変数を制御するコンポーネント(代わりにuseTransitionを使用します)
Suspenseとlazy
Suspenseコンポーネントは、(使用可能になった時点で)コードまたはデータの読み込み中にフォールバックコンテンツを表示します。lazy関数は、オンデマンドでコンポーネントをインポートします(必要に応じてコードを取得)。これらは単独で使用することも、一緒に使用することもできます。
Suspenseを単独で使用してデータを取得
この例では、MediaListがジャンルデータを取得してレンダリングしている間、Suspenseは読み込みプレースホルダー(MediaSkeleton)を表示します。
import { MediaAPI, Media, MediaCard } from "./api/mediaApi.ts";
type Props = { genre: string };
export const MediaList = ({ genre }: Props) => {
const [media, setMedia] = useState<Media[]>([]);
useEffect(() => {
// プロミスの解決時間が長い
MediaAPI.getGenre(genre)
.then(setMedia)
.catch((e) => console.error(e));
}, [genre]);
if (media.length===0) return null;
return media.map((media: Media) => (
<MediaCard key={media.id} media={media} />
));
};
lazyとSuspenseを併用し、既定の読み込み状態でコードを分割
この例では、LazyLoadedScreenのレンダリング時にMassiveComponentをインポートすると同時に、SuspenseがMediaSkeletonプレースホルダーを表示します。
const MassiveComponent = lazy(() => import('./MassiveComponent'));
export const LazyLoadedScreen = () => {
return (
<View style={styles.container}>
<Suspense fallback={<MediaSkeleton style={styles.skeleton} />}>
<MassiveComponent />
</Suspense>
</View>
);
};
並列レンダリングの実装
並列レンダリングを実装するには、次の手順に従います。
手順1: コンポーネントレベルのメモ化を追加
メモ化により、不要な再レンダリングと計算を防ぐことができます。コンポーネントの子では、useTransition()またはuseDeferredValue()を使用することが重要です。メモ化を行わないと、親が更新した時点で子が再レンダリングされるため、並列レンダリングのメリットが失われます。
メモ化しない場合:
この例のSearchComponentは、useTransitionをレンダリングする際、メモ化がどのように影響するかを示しています。メモ化しないと、結果に変更がない場合でも、キーストロークのたびにSearchResultsが再レンダリングされます。
const SearchComponent = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleSearch = (query) => {
setQuery(query); // 検索入力を更新
startTransition(() => {
setResults(performSearch(query)); // 検索操作を延期
});
};
// キーストロークのたびにSearchResultsが再レンダリングされます
const SearchResults = ({results}) => {
return results.map(result => <Card data={result} />);
};
return (
<View>
<TextInput
value={query}
onChange={handleSearch}
/>
{isPending && <LoadingIndicator />}
<SearchResults results={results} />
</View>
);
};
メモ化する場合:
memoを使用すると、検索が変更されたときのみSearchResultsが再レンダリングされます。
const SearchComponent = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleSearch = (query) => {
setQuery(query); // 検索入力を更新
startTransition(() => {
setResults(performSearch(query)); // 検索操作を延期
});
};
// 結果が変更されたときのみSearchResultsが再レンダリングされます
const SearchResults = memo(({results}) => {
return results.map(result => <Card data={result} />);
});
return (
<View>
<TextInput
value={query}
onChange={handleSearch}
/>
{isPending && <LoadingIndicator />}
<SearchResults results={results} />
</View>
);
};
手順2: 優先度ベースの更新を適用
入力、クリック、スクロールなどの操作はすぐにユーザーにフィードバックする必要がありますが、データ処理やUIの更新は遅らせることができます。更新に優先順位を付けるには、useTransitionとuseDeferredValueを使用します。
更新に優先順位を付けない場合:
const SearchComponent = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const handleSearch = (query) => {
setQuery(query); // 検索入力を更新
setResults(performSearch(query); // 検索中にUIをブロック
};
return (
<View>
<TextInput
value={query}
onChange={handleSearch}
/>
<SearchResults results={results} />
</View>
);
};
入力更新(setQuery)のたびに負荷の大きい操作(performSearch)が即座に実行され、両方の更新が完了するまでUIがブロックされます。キーストロークのたびに検索操作の完了を待つ必要があるため、ユーザーは入力時に遅延を感じます。
更新に優先順位を付ける場合:
const SearchComponent = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleSearch = (query) => {
setQuery(query); // 検索入力を更新
startTransition(() => {
setResults(performSearch(query); // 検索操作を延期
});
};
return (
<View>
<TextInput
value={query}
onChange={handleSearch}
/>
{isPending && <LoadingIndicator />}
// メモ化を忘れないこと
<SearchResults results={results} />
</View>
);
};
負荷の大きい操作(performSearch)がトランジションで実行されるようになったため、入力更新(setQuery)を即座に完了できます。検索結果がバックグラウンドで更新されている間、ユーザーはスムーズに入力できます。更新が保留されているときは読み込みインジケーターが表示されます。
テストと検証
並列レンダリングの実装をテストするには、以下のツールを使用します。
React DevToolsプロファイラー
React DevToolsプロファイラーを使用して、メモ化および優先更新の実装をテストします。
- Chromeで [React DevTools] を開きます。
- [Profiler] タブを選択します。
- 記録ボタンをクリックします。
- テストインタラクションを実行します(検索語句の入力、リストのスクロールなど)。
- 記録を停止します。
- フレームグラフに不要な再レンダリングやトランジションマーカーがないことを確認します。
Vega App KPI Visualizer
UIの応答性をテストするときは、Vega App KPI Visualizerを使用します。
Vega App KPI Visualizerを実行するには、次のコマンドを使用します。
kepler exec perf kpi-visualizer --app-name=[app-name] --kpi ui-fluidity --test-scenario <appiumを使用したpythonテストシナリオ>
滑らかさスコアが99%を超えており、並列レンダリングの実装後もスムーズなUIインタラクションを維持していることを確認します。
関連トピック
Last updated: 2025年11月5日

