【実務・中級編】 Concurrent Renderingの概念と仕組み – React実践ガイド

やあ。React 18のリリースからしばらく経つけれど、現場で「Concurrent Rendering(並行レンダリング)」を意識したコードを書けているエンジニアは、意外とまだ少ない。

多くのエンジニアにとって、Reactは「書けば勝手にいい感じに画面を更新してくれる魔法のライブラリ」のままだ。だが、君のような中級の壁を突破したいエンジニアには、そろそろ「ブラウザのメインスレッドをどう解放するか」という泥臭い戦場に足を踏み入れてほしい。

今日は、Reactの裏側で起きている「中断」と「優先順位」の真実について、現場の視点から解説するよ。

—

1. そもそも、なぜ「並行」が必要なのか?

これまでのReact(Legacyモード)は、一度レンダリングが始まると、それが終わるまでブラウザは一切の入力を受け付けなかった。いわゆる「ブロッキングレンダリング」だ。

例えば、巨大なリストを描画する処理が走っている最中に、ユーザーが検索窓に文字を入力したとする。ブラウザがその入力を反映できるのは、Reactの重たい計算が終わった後。結果として、「カクつくUI」が出来上がる。

Concurrent Renderingは、この「一度始めたら止まらない」という制約を破壊したんだ。「重要なタスク(入力など)」と「それほどでもないタスク(裏側での計算)」を区別し、レンダリング作業を一時停止・再開・破棄できるようにしたのが、この仕組みの核になる。

2. 現場で意識すべき「優先順位」の正体

Concurrent Renderingを制御する鍵は `useTransition` だ。これを使うと、Reactに対して「これは急ぎじゃないから、裏でやっておいてくれ」と伝えることができる。

実践的なサンプルコード

以下のコードは、検索入力とリストの絞り込みを同時に行う例だ。`useTransition` を使うことで、入力に対するレスポンスを維持したまま、重いリストのフィルタリングを裏側に回せる。

import React, { useState, useTransition } from ‘react’;

// 非常に重いフィルタリング処理をシミュレート
const heavyFilter = (data: string[]) => {
const start = performance.now();
// 意図的にメインスレッドを占有させる
while (performance.now() – start < 100) {} return data; }; export const SearchComponent = ({ items }: { items: string[] }) => {
const [query, setQuery] = useState(”);
const [isPending, startTransition] = useTransition();
const [filteredItems, setFilteredItems] = useState(items);

const handleChange = (e: React.ChangeEvent) => {
const value = e.target.value;
setQuery(value);

// startTransitionで囲むことで、この更新は「低優先」になる
// つまり、入力に対するstate更新(query)が最優先され、
// 画面のフリーズを防ぎつつ、裏でリストの更新を計算する
startTransition(() => {
const nextList = items.filter((item) => item.includes(value));
setFilteredItems(nextList);
});
};

return (


{/ 処理中であることを示すインジケーター /}
{isPending &&

検索中…

}

    {filteredItems.map((item) =>

  • {item}
  • )}

);
};

このコードの「何が」凄いのか

もし `startTransition` を使わなかったらどうなるか。`setQuery` と `setFilteredItems` が同じ優先度で処理されるため、`heavyFilter` が終わるまでブラウザは入力イベントを無視する。ユーザーから見れば「キーボードを叩いたのに文字が出ない」というストレスフルな体験になるわけだ。

3. ブラウザの中で起きている「分断」

内部的には、React 18以降、レンダリング作業は小さな「作業単位(Fiberノード単位)」に分割されている。

1. Fiberの構築: Reactはツリーを少しずつ走査する。
2. タイムスライスの確保: 一定時間が経過したら、Reactは「まだ作業は残っているけど、一旦ブラウザに制御を戻すね」と言って、メインスレッドを解放する。
3. ブラウザの介入: その隙に、ブラウザはユーザーのクリックや入力イベントを処理する。
4. 再開: イベント処理が終わったら、Reactは中断した場所からレンダリングを再開する。

これを「中断可能なレンダリング(Interruptible Rendering)」と呼ぶ。Reactは、現在のレンダリングよりも優先度の高いタスク(`startTransition` 以外の状態更新など)が来たら、今やっているレンダリングを捨てて、新しいタスクに飛びつくことさえあるんだ。

4. チーフアーキテクトからの助言

最後に、現場でこの技術を扱う上での注意点を一つ。

「Concurrent Renderingは魔法ではない」ということだ。`useTransition` を乱用すればいいというものではない。そもそも、レンダリングが重すぎる原因が「不必要なコンポーネントの再レンダリング」にあるなら、まずは `memo` や `useMemo` で計算量を減らすのが先だ。

Concurrent Renderingは、どうしても削れない重い処理があるときに、「ユーザー体験を損なわないための最後の砦」として使うものだと思ってほしい。

まずは自分のプロジェクトで、少し重い画面に対して `useTransition` を仕込んでみてくれ。ブラウザのパフォーマンス計測ツール(Profiler)を眺めながら、レンダリングが「分断」されている様子を見るのは、エンジニアにとって極上の体験になるはずだ。

何か詰まったら、いつでも聞いてくれ。アーキテクチャの泥沼は、一緒に掘り下げるのが一番楽しいからね。

コメント

タイトルとURLをコピーしました