react-render-optimization について
不要な再レンダーや描画コストを減らすための、React 開発者向け実践ガイドです。派生値、遅延更新、長いリストの描画などのパターンを扱い、性能問題の調査やコンポーネントのリファクタリングに役立ちます。
主な機能
- 01描画中に派生値を計算
- 02粗粒度の状態を購読
- 03高コストなサブツリーの分離とメモ化
- 04遅延状態初期化の利用
- 05高頻度の一時値を ref に保持
- 06startTransition による非緊急更新の遅延
活用シーン
- 01予期しないコンポーネント再レンダーの調査
- 02入力やスクロール時の応答性改善
- 03ダッシュボード、エディター、リストの最適化
- 04Profiler の結果に基づくコンポーネントの改修
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ patternsdev/ skills --skill react-render-optimizationインストール後、Claude Code で/react-render-optimizationを入力して読み込まれたか確認します。
詳細
React の再レンダリング最適化
目次
不要な再レンダリングをなくし、レンダリングコストを抑え、React UI の応答性を保つための実践的なパターンです。これらのパターンは、Vite、Next.js、Remix、または独自のセットアップを使用しているかどうかにかかわらず、あらゆる React アプリケーションに適用できます。
使用する場面
次のような場合に、これらのパターンを参考にしてください。
- コンポーネントが想定より頻繁に再レンダリングされる
- 入力、スクロール、操作中に UI の反応が鈍く感じられる
- プロファイラーでコンポーネントツリー内の無駄なレンダリングが確認される
- パフォーマンスが重要な機能(ダッシュボード、エディター、リスト)を構築する
- 既存の React コンポーネントをレビューまたはリファクタリングする
手順
- コードの生成、レビュー、リファクタリング時にこれらのパターンを適用してください。アンチパターンを見つけたら、修正版を説明とともに提案してください。
詳細
概要
React は、コンポーネントの状態が変化したとき、親コンポーネントが再レンダリングされたとき、またはコンポーネントが利用するコンテキストが更新されたときに、そのコンポーネントを再レンダリングします。ほとんどの再レンダリングは問題ありませんが、コストの高い計算、深いツリー、レイアウトの頻繁な再計算を引き起こすと、ユーザーにも影響が見えるようになります。
以下のパターンは効果の大きい順に並んでいます。細かな最適化に取り組む前に、まず効果の大きいものから対処してください。
1. 派生値はレンダリング中に計算する — 保存しない
効果:高 — バグと不要な状態を生み出す問題をまとめて解消できます。
既存の状態や props から計算できる値を保存すると、同期の問題や余計な再レンダリングが発生します。代わりに、インラインで計算してください。
避ける — ずれが生じる冗長な状態:
function ProductList({ products }: { products: Product[] }) {
const [search, setSearch] = useState('')
const [filtered, setFiltered] = useState(products)
useEffect(() => {
setFiltered(products.filter(p =>
p.name.toLowerCase().includes(search.toLowerCase())
))
}, [products, search])
return (
<>
<input value={search} onChange={e => setSearch(e.target.value)} />
{filtered.map(p => <ProductCard key={p.id} product={p} />)}
</>
)
}
推奨 — レンダリング中に導出する(簡単な導出には通常の const を使用):
function ProductList({ products }: { products: Product[] }) {
const [search, setSearch] = useState('')
// Cheap derivation — plain const, no useMemo needed
const hasSearch = search.length > 0
const normalizedSearch = search.toLowerCase()
// Expensive derivation — useMemo is justified when iterating large arrays
const filtered = useMemo(
() => products.filter(p =>
p.name.toLowerCase().includes(normalizedSearch)
),
[products, normalizedSearch]
)
return (
<>
<input value={search} onChange={e => setSearch(e.target.value)} />
{hasSearch && <ClearButton />}
{filtered.map(p => <ProductCard key={p.id} product={p} />)}
</>
)
}
useMemo と通常の const の使い分け:
- 通常の
const— 真偽値のフラグ、文字列の整形、単純な算術演算、オブジェクトのプロパティアクセス、.lengthの確認。これらは実質的にコストがかからないため、useMemoのオーバーヘッドに見合いません。 useMemo— 配列のフィルタリングやソート、データ構造の構築、JSON.parse、コストの高い変換、コレクションの反復や O(n) の処理を伴うもの。
ルール:式がプリミティブ値を返す場合、または単一のプロパティアクセスである場合は、useMemo を使わないでください。データを反復または変換する場合は、式をラップしてください。
React Compiler に関する注記: React Compiler が有効な場合、式は自動的にメモ化されるため、手動で
useMemoを呼び出す必要はありません。
2. 生の値ではなく、大まかな状態を購読する
効果:高 — 関係のない変更による再レンダリングを防ぎます。
コンポーネントが派生した真偽値(例:「モバイルかどうか」)だけを必要とする場合は、連続的に変化する生の値を購読しないでください。
避ける — ピクセルごとに再レンダリング:
function Sidebar() {
const width = useWindowWidth() // fires on every resize
const isMobile = width < 768
return <nav className={isMobile ? 'mobile' : 'desktop'}>...</nav>
}
推奨 — 真偽値が切り替わったときだけ再レンダリング:
function Sidebar() {
const isMobile = useMediaQuery('(max-width: 767px)')
return <nav className={isMobile ? 'mobile' : 'desktop'}>...</nav>
}
これは幅広く応用できます。ユーザーオブジェクト全体ではなく isLoggedIn を、カート配列全体ではなく hasItems を購読する、といった具合です。
3. コストの高いサブツリーをメモ化したコンポーネントに切り出す
効果:高 — 早期リターンによるレンダリングのスキップを可能にします。
親コンポーネントに高速処理(読み込み中、エラー、空の状態)があっても、コストの高い子コンポーネントが同じコンポーネント内にあると計算が行われます。子コンポーネントを切り出して、React がそのレンダリング全体をスキップできるようにしてください。
避ける — 読み込み中にもアバターの計算が実行される:
function Profile({ user, loading }: Props) {
const avatar = useMemo(() => processAvatar(user), [user])
if (loading) return <Skeleton />
return <div><img src={avatar} /></div>
}
推奨 — 読み込み中は計算をスキップ:
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
const avatar = useMemo(() => processAvatar(user), [user])
return <img src={avatar} />
})
function Profile({ user, loading }: Props) {
if (loading) return <Skeleton />
return <div><UserAvatar user={user} /></div>
}
React Compiler に関する注記: コンパイラーによって自動的にメモ化されるため、手動で
memo()を付ける必要性は低くなります。ただし、早期リターンのためにコンポーネントを切り出すことには、引き続き価値があります。
4. 遅延状態初期化を使用する
影響:中 — レンダリングのたびに発生する無駄な計算を避けます。
useState に初期値として関数呼び出しを渡すと、その結果は一度しか使われないにもかかわらず、レンダリングのたびに関数が実行されます。代わりに、関数参照を渡します。
避ける — buildIndex() がレンダリングのたびに実行されます:
const [index, setIndex] = useState(buildSearchIndex(items))
推奨 — マウント時に一度だけ実行されます:
const [index, setIndex] = useState(() => buildSearchIndex(items))
遅延初期化は、JSON.parse、localStorage の読み取り、データ構造の構築、負荷の高い変換に使用します。useState(0) や useState(false) のような単純なプリミティブ値では不要です。
5. 安定したコールバックには関数型 setState を使用する
影響:中 — 依存配列から状態変数を取り除けます。
次の状態を計算するために以前の状態だけが必要なコールバックでは、関数型を使用します。これにより、依存配列から状態変数を取り除き、コールバックの識別性を安定させられます。
避ける — count が変わるとコールバックも変わります:
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount(count + 1), [count])
推奨 — コールバックは常に安定しています:
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount(c => c + 1), [])
6. インタラクションのロジックはエフェクトではなくイベントハンドラーに記述する
影響:中 — 依存値の変更時に副作用が再実行されるのを避けます。
ユーザーの操作(クリック、送信、ドラッグ)が副作用を引き起こす場合は、イベントハンドラー内で実行します。状態とエフェクトで表現すると、無関係な依存値の変更時にも再実行されます。
避ける — theme が変わるとエフェクトが再実行されます:
function Form() {
const [submitted, setSubmitted] = useState(false)
const theme = useContext(ThemeContext)
useEffect(() => {
if (submitted) {
post('/api/register')
showToast('Registered', theme)
}
}, [submitted, theme])
return <button onClick={() => setSubmitted(true)}>Submit</button>
}
推奨 — ハンドラー内にロジックを記述します:
function Form() {
const theme = useContext(ThemeContext)
function handleSubmit() {
post('/api/register')
showToast('Registered', theme)
}
return <button onClick={handleSubmit}>Submit</button>
}
7. 一時的で更新頻度の高い値には useRef を使用する
影響:中 — 頻繁な更新による再レンダリングを防ぎます。
マウス位置、スクロール位置、インターバルのティックなど、非常に頻繁に変化する一方で、再レンダリングを引き起こす必要のない値は ref に保存します。必要に応じて DOM を直接更新します。
避ける — マウスを動かすたびに再レンダリングされます:
function Cursor() {
const [x, setX] = useState(0)
useEffect(() => {
const handler = (e: MouseEvent) => setX(e.clientX)
window.addEventListener('mousemove', handler)
return () => window.removeEventListener('mousemove', handler)
}, [])
return <div style={{ transform: `translateX(${x}px)` }} />
}
推奨 — 再レンダリングは発生しません:
function Cursor() {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const handler = (e: MouseEvent) => {
if (ref.current) {
ref.current.style.transform = `translateX(${e.clientX}px)`
}
}
window.addEventListener('mousemove', handler)
return () => window.removeEventListener('mousemove', handler)
}, [])
return <div ref={ref} />
}
8. 緊急性の低い更新には startTransition を使用する
影響:中 — 優先度の高い更新(入力、クリック)の応答性を保ちます。
緊急性の低い状態更新を startTransition で囲むと、React は緊急性の高い処理を優先して割り込めるようになります。検索結果の絞り込み、タブの切り替え、リストの並べ替えで特に役立ちます。
避ける — リストの絞り込み中は入力がブロックされます:
function Search({ items }: { items: Item[] }) {
const [query, setQuery] = useState('')
const [filtered, setFiltered] = useState(items)
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value)
setFiltered(items.filter(i => i.name.includes(e.target.value)))
}
return (
<>
<input value={query} onChange={handleChange} />
<List items={filtered} />
</>
)
}
推奨 — 入力の応答性が保たれます:
import { useState, useTransition } from 'react'
function Search({ items }: { items: Item[] }) {
const [query, setQuery] = useState('')
const [filtered, setFiltered] = useState(items)
const [isPending, startTransition] = useTransition()
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value)
startTransition(() => {
setFiltered(items.filter(i => i.name.includes(e.target.value)))
})
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<List items={filtered} />
</>
)
}
9. 状態の読み取りは必要な箇所まで遅らせる
影響:中 — コールバック内でしか読まない状態へのサブスクリプションを避けます。
イベントハンドラー内でのみ値を読み取る場合は、useSearchParams() のようなフックを呼び出さないでください。必要になった時点で読み取ります。
避ける — URL が変わるたびにコンポーネントが再レンダリングされます:
function ShareButton({ id }: { id: string }) {
const [searchParams] = useSearchParams()
const handleShare = () => {
const ref = searchParams.get('ref')
share(id, { ref })
}
return <button onClick={handleShare}>Share</button>
}
推奨 — 必要な時点で読み取り、サブスクリプションは発生しません:
function ShareButton({ id }: { id: string }) {
const handleShare = () => {
const params = new URLSearchParams(window.location.search)
share(id, { ref: params.get('ref') })
}
return <button onClick={handleShare}>Share</button>
}
10. デフォルトの props には安定した参照を使用する
影響:中 — 新しいオブジェクトや配列のリテラルによって memo() の効果が失われるのを防ぎます。
デフォルトの prop 値として [] や {} を渡すと、レンダリングのたびに新しい参照が作成され、子コンポーネントのメモ化の効果が失われます。
避ける — レンダリングのたびに新しい配列が作成されます:
function Dashboard({ tabs = [] }: { tabs?: Tab[] }) {
return <TabBar tabs={tabs} /> {/* TabBar re-renders every time */}
}
推奨 — 安定した参照:
const EMPTY_TABS: Tab[] = []
function Dashboard({ tabs = EMPTY_TABS }: { tabs?: Tab[] }) {
return <TabBar tabs={tabs} />
}
11. 長いリストには CSS の content-visibility を使用する
影響:高 — スクロール可能な長いコンテンツの初回レンダリングが 5~10 倍高速になります。
画面外のアイテムに content-visibility: auto を適用すると、ブラウザーはそれらが画面内にスクロールされるまで、レイアウトと描画をスキップします。
.list-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px; /* estimated height */
}
function MessageList({ messages }: { messages: Message[] }) {
return (
<div style={{ overflowY: 'auto', height: '100vh' }}>
{messages.map(msg => (
<div key={msg.id} className="list-item">
<MessageCard message={msg} />
</div>
))}
</div>
)
}
1,000 件のアイテムがある場合、ブラウザーは画面外にある約 990 件のアイテムのレイアウトと描画をスキップします。非常に大規模なリストでは、仮想化(例:react-window、@tanstack/react-virtual)と組み合わせてください。
12. 静的な JSX をコンポーネントの外に移動する
影響:低 — 同一の要素を再作成するのを避けます。
変化しない JSX 要素は、モジュールスコープに移動できます。React はレンダリング間で同じオブジェクト参照を再利用します。
避ける — レンダリングのたびに再作成されます:
function Page() {
return (
<main>
<footer>
<p>Copyright 2026 Acme Inc.</p>
</footer>
</main>
)
}
推奨 — 一度だけ作成されます:
const footer = (
<footer>
<p>Copyright 2026 Acme Inc.</p>
</footer>
)
function Page() {
return <main>{footer}</main>
}
再作成にコストがかかる大きな SVG 要素で、特に効果があります。
React Compiler に関する注記: コンパイラーは静的な JSX を自動的に外に移動するため、この手動の最適化は不要になります。
13. アプリごとにコストの高い処理を一度だけ初期化する
影響:低〜中 — Strict Mode や再マウント時の初期化の重複を防ぎます。
アプリ全体の初期化処理(分析、認証チェック、サービスワーカーなど)を useEffect に置くべきではありません。開発時や並行機能の使用時には、コンポーネントが再マウントされるためです。モジュールレベルのガードを使用してください。
避けるべき例 — 開発時に2回実行され、再マウント時にも再実行されます:
function App() {
useEffect(() => {
initAnalytics()
checkAuth()
}, [])
return <Router />
}
推奨 — アプリの読み込みごとに一度だけ実行します:
let initialized = false
function App() {
useEffect(() => {
if (initialized) return
initialized = true
initAnalytics()
checkAuth()
}, [])
return <Router />
}
または、エントリーファイル(main.tsx)で、コンポーネントの外にあるモジュールレベルで初期化してください。
14. 安定したサブスクリプションのため、イベントハンドラーを ref に保存する
影響:低 — エフェクトの再サブスクリプションを防ぎます。
カスタムフックでイベントをサブスクライブし、コールバックを受け取る場合は、レンダーのたびにサブスクリプションが解除・再作成されないよう、コールバックを ref に保存してください。
避けるべき例 — ハンドラーが変わるたびに再サブスクライブします:
function useWindowEvent(event: string, handler: (e: Event) => void) {
useEffect(() => {
window.addEventListener(event, handler)
return () => window.removeEventListener(event, handler)
}, [event, handler])
}
推奨 — 安定したサブスクリプション:
function useWindowEvent(event: string, handler: (e: Event) => void) {
const saved = useRef(handler)
useEffect(() => { saved.current = handler }, [handler])
useEffect(() => {
const listener = (e: Event) => saved.current(e)
window.addEventListener(event, listener)
return () => window.removeEventListener(event, listener)
}, [event])
}
React 19 以降では、組み込みのパターンとして useEffectEvent を使用できます:
import { useEffectEvent } from 'react'
function useWindowEvent(event: string, handler: (e: Event) => void) {
const onEvent = useEffectEvent(handler)
useEffect(() => {
window.addEventListener(event, onEvent)
return () => window.removeEventListener(event, onEvent)
}, [event])
}
15. クライアント専用データのハイドレーション時のちらつきを防ぐ
影響:中 — SSR のハイドレーション中に誤ったコンテンツが一瞬表示されるのを防ぎます。
レンダリングがクライアント専用データ(localStorage、Cookie)に依存している場合、インラインスクリプトで React のハイドレーション前に正しい値を設定できます。これにより、SSR エラーと画面上でのちらつきの両方を防げます。
function ThemeRoot({ children }: { children: React.ReactNode }) {
return (
<>
<div id="app-root">{children}</div>
<script
dangerouslySetInnerHTML={{
__html: `(function(){
try {
var t = localStorage.getItem('theme') || 'light';
document.getElementById('app-root').dataset.theme = t;
} catch(e) {}
})();`,
}}
/>
</>
)
}
この方法は、Next.js、Remix、カスタムの Vite SSR パイプラインなど、あらゆる SSR 構成で機能します。
16. コンポーネント内でコンポーネントを定義しない
影響:高 — レンダーのたびに再マウント、状態の消失、不要な DOM 処理が発生します。
あるコンポーネントのレンダー内で別のコンポーネントを定義すると、React はレンダーのたびに新しいコンポーネント型を作成します。その結果、サブツリー全体がアンマウントおよび再マウントされ、すべての状態、DOM ノード、エフェクトのクリーンアップとセットアップが失われます。
避けるべき例 — Row はレンダーのたびに新しい型になります:
function Table({ data }: { data: Item[] }) {
// This creates a NEW component type on every render
function Row({ item }: { item: Item }) {
const [selected, setSelected] = useState(false)
return <tr onClick={() => setSelected(!selected)}>{item.name}</tr>
}
return <table>{data.map(item => <Row key={item.id} item={item} />)}</table>
}
推奨 — Row をモジュールスコープで定義します:
function Row({ item }: { item: Item }) {
const [selected, setSelected] = useState(false)
return <tr onClick={() => setSelected(!selected)}>{item.name}</tr>
}
function Table({ data }: { data: Item[] }) {
return <table>{data.map(item => <Row key={item.id} item={item} />)}</table>
}
これは useMemo、useCallback、その他のフック内で定義したコンポーネントにも当てはまります。コンポーネントは常にモジュールスコープ、または静的プロパティとして定義してください。
17. コストの高い派生レンダーには useDeferredValue を使用する
影響:高 — コストの高いサブツリーをバックグラウンドで再レンダーしている間も、UI の応答性を保ちます。
useDeferredValue は、変化の速い値に依存するコンポーネントの再レンダーを React に遅延させます。状態の更新をラップする useTransition とは異なり、useDeferredValue は値の使用をラップします。状態セッターを制御できない場合に便利です。
避けるべき例 — キー入力のたびに UI がブロックされます:
function SearchPage({ query }: { query: string }) {
// Expensive: filters and renders 10,000 items on every keystroke
const results = filterItems(query)
return <ResultsList items={results} />
}
推奨 — 入力への応答性を保ち、結果はバックグラウンドで更新されます:
import { useDeferredValue, useMemo } from 'react'
function SearchPage({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query)
const isStale = query !== deferredQuery
const results = useMemo(() => filterItems(deferredQuery), [deferredQuery])
return (
<div style={{ opacity: isStale ? 0.7 : 1 }}>
<ResultsList items={results} />
</div>
)
}
useDeferredValue と useTransition の使い分け:
useTransition— 状態セッターを制御でき、startTransitionでラップできる場合useDeferredValue— 値が props、親コンポーネント、または制御できないライブラリから渡される場合
18. 条件付きレンダリングでは明示的なチェックを使用する
影響:中 — 0、NaN、空文字列が DOM にレンダリングされるのを防ぎます。
JSX の && 演算子は、偽値に対して短絡評価します。しかし、0、NaN、"" は偽値でありながら、表示されるテキストノードとしてレンダリングされます。
避けるべき例 — count がゼロのとき、0 が DOM にレンダリングされます:
function NotificationBadge({ count }: { count: number }) {
return <div>{count && <Badge>{count}</Badge>}</div>
// When count is 0, renders: <div>0</div>
}
推奨 — 明示的な真偽値チェック:
function NotificationBadge({ count }: { count: number }) {
return <div>{count > 0 && <Badge>{count}</Badge>}</div>
}
// Or use a ternary for clarity
function NotificationBadge({ count }: { count: number }) {
return <div>{count > 0 ? <Badge>{count}</Badge> : null}</div>
}
これは、配列の長さ、文字列値、数値 props など、0、NaN、"" になる可能性があるあらゆる値に当てはまります。真偽値の評価に頼るのではなく、常に明示的な真偽値の式(> 0、!== ''、!= null)を使用してください。
19. エフェクトの依存関係をプリミティブ値に絞る
影響:中 — 関連のないオブジェクトプロパティが変わったときに、エフェクトが再実行されるのを防ぎます。
エフェクトでオブジェクトのプロパティを1つだけ使う場合は、依存配列の前に取り出します。オブジェクト全体を渡すと、いずれかのプロパティが変わるたびに再実行されます。
避ける — user.name または user.avatar が変わるとエフェクトが再実行されます:
function UserStatus({ user }: { user: User }) {
useEffect(() => {
updatePresence(user.id)
}, [user]) // re-runs on ANY user property change
}
推奨 — ID が変わったときだけ再実行されます:
function UserStatus({ user }: { user: User }) {
const { id } = user
useEffect(() => {
updatePresence(id)
}, [id])
}
これはフックの戻り値にも当てはまります。useQuery が { data, status, fetchStatus } を返し、エフェクトで status だけを使う場合は、先に分割代入します。
20. 複合されたフックの計算を分割する
影響:中 — フックの出力の一部だけを必要とするコンシューマーの再レンダリングを防ぎます。
カスタムフックが関連のない複数の値を計算する場合、1つの値が変わると、変わっていない値だけを参照するコンシューマーも含め、すべてのコンシューマーが再レンダリングされます。
避ける — total が変わると、average だけを必要とするコンポーネントも再レンダリングされます:
function useStats(items: number[]) {
return useMemo(() => ({
total: items.reduce((a, b) => a + b, 0),
average: items.reduce((a, b) => a + b, 0) / items.length,
max: Math.max(...items),
}), [items])
}
推奨 — 用途を絞ったフックに分割します:
function useTotal(items: number[]) {
return useMemo(() => items.reduce((a, b) => a + b, 0), [items])
}
function useAverage(items: number[]) {
return useMemo(() => items.reduce((a, b) => a + b, 0) / items.length, [items])
}
function useMax(items: number[]) {
return useMemo(() => Math.max(...items), [items])
}
コンポーネントは必要なフックだけを呼び出します。1つのコンポーネントですべての値が必要な場合は、そこでまとめても問題ありません。この分割により、フックのレベルで不要な結びつきを防げます。
21. DOM の読み取りと書き込みをまとめてレイアウトスラッシングを防ぐ
影響:高 — メインスレッドをブロックする、強制的な同期レイアウトを防ぎます。
DOM への書き込み後にレイアウトプロパティ(例:offsetHeight、getBoundingClientRect())を読み取ると、ブラウザーはレイアウトを同期的に再計算する必要があります。これをループ内で行うと、レイアウトスラッシングが発生します。
避ける — 反復のたびにレイアウトの再計算を強制します:
function resizeCards(cards: HTMLElement[]) {
cards.forEach(card => {
const height = card.offsetHeight // READ (forces layout)
card.style.minHeight = `${height + 20}px` // WRITE (invalidates layout)
})
}
推奨 — 読み取りをすべて行ってから、書き込みをすべて行います:
function resizeCards(cards: HTMLElement[]) {
// Read phase
const heights = cards.map(card => card.offsetHeight)
// Write phase
cards.forEach((card, i) => {
card.style.minHeight = `${heights[i] + 20}px`
})
}
React では、これは DOM 要素の測定と変更を行う useLayoutEffect または useEffect のコールバックでよく発生します。アニメーションフレーム内でレイアウトを読み取る必要がある場合は、requestAnimationFrame を使ってまとめます:
useLayoutEffect(() => {
const measurements = items.map(el => el.getBoundingClientRect())
requestAnimationFrame(() => {
items.forEach((el, i) => {
el.style.transform = `translateY(${measurements[i].top}px)`
})
})
}, [items])
22. SVG 要素を直接ではなく、ラッパーをアニメーションする
影響:中 — アニメーションフレームごとに SVG 全体が再描画されるのを防ぎます。
SVG 要素自体(例:<svg> または <path>)のプロパティをアニメーションすると、SVG 全体が再描画されます。SVG を <div> で囲み、代わりにラッパーをアニメーションします。
避ける — SVG ツリー全体が再描画されます:
<motion.svg animate={{ rotate: 360 }} style={{ width: 200, height: 200 }}>
<ComplexChart />
</motion.svg>
推奨 — ラッパーだけが再描画されます:
<motion.div animate={{ rotate: 360 }} style={{ width: 200, height: 200 }}>
<svg viewBox="0 0 200 200">
<ComplexChart />
</svg>
</motion.div>
これは CSS アニメーションにも当てはまります。cx、cy、d などの SVG 属性を直接アニメーションするのではなく、ラッパー要素に transform を使います。
23. 想定内のハイドレーション不一致を抑制する
影響:低~中 — 既知の安全な警告を表示しないようにしつつ、本当のバグは隠しません。
サーバーとクライアントで意図的に内容が異なる場合があります。たとえば、タイムスタンプ、ランダム ID、ユーザーエージェントに応じたレンダリングです。該当する要素に suppressHydrationWarning を使います。
function Comment({ createdAt }: { createdAt: Date }) {
return (
<article>
<p>{comment.body}</p>
<time suppressHydrationWarning>
{formatRelativeTime(createdAt)} {/* "2 minutes ago" differs server vs client */}
</time>
</article>
)
}
使用は最小限にとどめ、リーフ要素にのみ適用します。コンテナー要素で警告を抑制しないでください。子要素にある本当の不一致が隠れてしまいます。
24. Vite の SPA で React DOM のリソースヒントを使う
影響:高 — フレームワークのサポートがなくても、ブラウザーが重要なリソースの読み込みをより早く開始できるようにします。
React 19 では、react-dom の preload() と preinit() が追加されました。これは、あらゆる React アプリで使える命令型のリソースヒントです。フレームワークレベルのプリフェッチがない Vite の SPA では、特に有用です。
import { preload, preinit } from 'react-dom'
function App() {
// Preload a font before it's needed
preload('/fonts/inter-var.woff2', { as: 'font', type: 'font/woff2', crossOrigin: 'anonymous' })
// Preinit a critical CSS file (loads + applies it)
preinit('/critical.css', { as: 'style' })
return <RouterProvider router={router} />
}
ナビゲーション時 — 次のページのデータとコードをプリロードします:
function ProductLink({ id }: { id: string }) {
const handleHover = () => {
// Preload the image the next page will need
preload(`/api/products/${id}/image.webp`, { as: 'image' })
// Prefetch the route code
import('./pages/ProductDetail')
}
return <Link to={`/products/${id}`} onMouseEnter={handleHover}>View</Link>
}
リソースがすでに読み込まれている場合、これらは何もしないため、早めに呼び出しても安全です。メタフレームワークを使わない Vite アプリでは、これがリソースの優先順位付けを行う主な方法です。
25. ルートナビゲーションには useTransition を使う
影響:中 — 次のルートの読み込み中も、現在のページを操作できる状態に保ちます。
React.lazy() ルートを使う Vite の SPA では、ナビゲーションリンクをクリックすると、チャンクの読み込みとコンポーネントのレンダリング中に UI がフリーズすることがあります。ナビゲーションを startTransition でラップすると、新しいページの準備が整うまで React が古いページを表示します。
import { useTransition } from 'react'
import { useNavigate } from 'react-router-dom'
function NavLink({ to, children }: { to: string; children: React.ReactNode }) {
const navigate = useNavigate()
const [isPending, startTransition] = useTransition()
const handleClick = (e: React.MouseEvent) => {
e.preventDefault()
startTransition(() => {
navigate(to)
})
}
return (
<a
href={to}
onClick={handleClick}
style={{ opacity: isPending ? 0.7 : 1 }}
>
{children}
</a>
)
}
これにより、遅延読み込みされたルート間で画面が一瞬空白になるのを防ぎ、isPending を使って現在のページに控えめな読み込みインジケーターを表示できます。
出典
patterns.dev のパターン — より広範なウェブエンジニアリングコミュニティ向けの、フレームワークに依存しない React パフォーマンスガイダンスです。