Zustand の再レンダリング最適化:selector と shallow 比較でパフォーマンス向上

React アプリケーションのパフォーマンス最適化において、不要な再レンダリングの削減は重要な課題です。Zustand を使用する際も例外ではありません。適切な selector の使用と shallow 比較の活用により、アプリケーションのパフォーマンスを大幅に向上させることができます。
本記事では、Zustand における再レンダリング最適化の具体的な手法を、実践的なコード例とともに詳しく解説いたします。
React の再レンダリング問題と Zustand
React の再レンダリングメカニズム
React コンポーネントは、state や props が変更されると再レンダリングが発生します。この仕組み自体は正常な動作ですが、不要な再レンダリングが頻発すると、アプリケーションのパフォーマンスに深刻な影響を与えます。
typescript// 問題のあるパターン:全てのstateを取得
const MyComponent = () => {
const store = useStore(); // ストア全体を取得
return (
<div>
<p>{store.user.name}</p>
{/* store内の任意の値が変更されると、このコンポーネントも再レンダリング */}
</div>
);
};
Zustand での再レンダリング発生条件
Zustand では、useStore
フックを使用してストアにアクセスします。デフォルトでは、ストア内の任意の値が変更されると、そのストアを参照している全てのコンポーネントが再レンダリングされます。
typescriptimport { create } from 'zustand';
interface AppState {
user: {
name: string;
email: string;
};
posts: Post[];
ui: {
isLoading: boolean;
theme: 'light' | 'dark';
};
}
const useStore = create<AppState>((set) => ({
user: { name: '', email: '' },
posts: [],
ui: { isLoading: false, theme: 'light' },
// actions...
}));
上記のストア構造で、ui.theme
が変更されただけでも、user.name
のみを使用しているコンポーネントまで再レンダリングされてしまいます。
パフォーマンス問題の具体例
実際のアプリケーションでよく見られる問題パターンを見てみましょう。
typescript// 非効率なパターン
const UserProfile = () => {
const { user, posts, ui } = useStore(); // 全てを取得
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
};
const PostList = () => {
const { posts, ui } = useStore(); // 必要以上に取得
return (
<div>
{posts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
};
この例では、user
情報が更新されるとPostList
コンポーネントも再レンダリングされ、posts
が更新されるとUserProfile
コンポーネントも再レンダリングされてしまいます。
パフォーマンス問題の特定方法
React DevTools を活用した分析
再レンダリング問題を特定するには、React DevTools の Profiler 機能が非常に有効です。
typescript// デバッグ用のカスタムフック
const useRenderCount = (componentName: string) => {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
console.log(
`${componentName} rendered: ${renderCount.current} times`
);
});
return renderCount.current;
};
const MyComponent = () => {
const renderCount = useRenderCount('MyComponent');
const user = useStore((state) => state.user);
return <div>Render count: {renderCount}</div>;
};
パフォーマンス測定の実装
より詳細な分析のために、カスタムフックでレンダリング時間を測定できます。
typescriptconst usePerformanceMonitor = (componentName: string) => {
const startTime = useRef<number>();
// レンダリング開始時刻を記録
startTime.current = performance.now();
useEffect(() => {
const endTime = performance.now();
const renderTime = endTime - startTime.current;
if (renderTime > 16) {
// 60fps基準
console.warn(
`${componentName}: Slow render (${renderTime.toFixed(
2
)}ms)`
);
}
});
};
ストア変更の追跡
Zustand の subscribe 機能を使用して、ストアの変更を詳細に追跡できます。
typescript// ストア変更の監視
useStore.subscribe((state, prevState) => {
const changedKeys = Object.keys(state).filter(
(key) => state[key] !== prevState[key]
);
console.log('Changed keys:', changedKeys);
});
selector による最適化戦略
基本的な selector の使用
selector を使用することで、必要な部分のみを取得し、不要な再レンダリングを防げます。
typescript// 最適化されたパターン
const UserProfile = () => {
// userオブジェクトのみを取得
const user = useStore((state) => state.user);
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
};
const PostList = () => {
// postsのみを取得
const posts = useStore((state) => state.posts);
return (
<div>
{posts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
};
複数値の効率的な取得
複数の値が必要な場合は、オブジェクトとして返す selector を作成します。
typescript// 複数値を効率的に取得
const UserDashboard = () => {
const { user, isLoading } = useStore((state) => ({
user: state.user,
isLoading: state.ui.isLoading,
}));
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>Welcome, {user.name}!</h1>
</div>
);
};
計算済み値の selector
selector では計算処理も行えます。ただし、パフォーマンスに注意が必要です。
typescript// 計算済み値を返すselector
const PostStats = () => {
const stats = useStore((state) => ({
totalPosts: state.posts.length,
publishedPosts: state.posts.filter((p) => p.published)
.length,
draftPosts: state.posts.filter((p) => !p.published)
.length,
}));
return (
<div>
<p>Total: {stats.totalPosts}</p>
<p>Published: {stats.publishedPosts}</p>
<p>Drafts: {stats.draftPosts}</p>
</div>
);
};
selector のメモ化
計算コストの高い selector は、useMemo でメモ化することを検討しましょう。
typescriptconst ExpensiveComponent = () => {
const expensiveData = useStore(
useCallback(
(state) =>
state.largeDataSet.map((item) => ({
...item,
processedValue: expensiveCalculation(item.value),
})),
[]
)
);
return (
<div>
{expensiveData.map((item) => (
<div key={item.id}>{item.processedValue}</div>
))}
</div>
);
};
shallow 比較の仕組みと活用法
shallow 比較とは
shallow 比較は、オブジェクトの第一階層のプロパティのみを比較する手法です。Zustand ではshallow
関数を使用して実装できます。
typescriptimport { shallow } from 'zustand/shallow';
// shallow比較を使用した最適化
const UserInfo = () => {
const { name, email } = useStore(
(state) => ({
name: state.user.name,
email: state.user.email,
}),
shallow
);
return (
<div>
<p>Name: {name}</p>
<p>Email: {email}</p>
</div>
);
};
shallow 比較の動作原理
shallow 比較がどのように動作するかを理解しましょう。
typescript// shallow比較の内部実装(簡略版)
function shallow(objA: any, objB: any): boolean {
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
for (let i = 0; i < keysA.length; i++) {
const key = keysA[i];
if (objA[key] !== objB[key]) {
return false;
}
}
return true;
}
配列での shallow 比較活用
配列を扱う際の shallow 比較の効果的な使用方法です。
typescript// 配列のshallow比較
const TagList = () => {
const tags = useStore(
(state) => state.post.tags,
shallow
);
return (
<div>
{tags.map((tag) => (
<span key={tag} className='tag'>
{tag}
</span>
))}
</div>
);
};
// オブジェクト配列での活用
const UserList = () => {
const users = useStore(
(state) =>
state.users.map((user) => ({
id: user.id,
name: user.name,
isActive: user.isActive,
})),
shallow
);
return (
<div>
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}
</div>
);
};
shallow 比較の注意点
shallow 比較には制限があることを理解しておきましょう。
typescript// shallow比較では検出できないケース
const DeepObjectComponent = () => {
// ネストしたオブジェクトの変更は検出されない
const userSettings = useStore(
(state) => state.user.settings, // settings内の変更は検出されない
shallow
);
return <div>{userSettings.theme}</div>;
};
// 解決策:必要な値を直接取得
const OptimizedDeepComponent = () => {
const theme = useStore(
(state) => state.user.settings.theme
);
return <div>{theme}</div>;
};
selector × shallow の組み合わせテクニック
基本的な組み合わせパターン
selector と shallow 比較を組み合わせることで、より効率的な最適化が可能です。
typescript// 基本的な組み合わせ
const UserDashboard = () => {
const userInfo = useStore(
(state) => ({
name: state.user.name,
email: state.user.email,
avatar: state.user.avatar,
lastLogin: state.user.lastLogin,
}),
shallow
);
return (
<div className='user-dashboard'>
<img src={userInfo.avatar} alt='Avatar' />
<h2>{userInfo.name}</h2>
<p>{userInfo.email}</p>
<small>Last login: {userInfo.lastLogin}</small>
</div>
);
};
条件付き selector
条件に応じて異なる値を取得する selector パターンです。
typescript// 条件付きselector
const ConditionalComponent = () => {
const data = useStore((state) => {
if (state.user.role === 'admin') {
return {
name: state.user.name,
permissions: state.user.adminPermissions,
dashboardType: 'admin',
};
} else {
return {
name: state.user.name,
preferences: state.user.preferences,
dashboardType: 'user',
};
}
}, shallow);
return (
<div>
<h1>Welcome, {data.name}</h1>
{data.dashboardType === 'admin' ? (
<AdminPanel permissions={data.permissions} />
) : (
<UserPanel preferences={data.preferences} />
)}
</div>
);
};
配列フィルタリングの最適化
配列のフィルタリングと shallow 比較を組み合わせた最適化手法です。
typescript// フィルタリング結果のメモ化
const FilteredPostList = ({
category,
}: {
category: string;
}) => {
const filteredPosts = useStore(
(state) =>
state.posts
.filter((post) => post.category === category)
.map((post) => ({
id: post.id,
title: post.title,
excerpt: post.excerpt,
publishedAt: post.publishedAt,
})),
shallow
);
return (
<div>
{filteredPosts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
);
};
カスタム比較関数の実装
特殊なケースでは、カスタム比較関数を実装することも可能です。
typescript// カスタム比較関数
const customEqual = (a: any, b: any) => {
// 特定のプロパティのみを比較
return a.id === b.id && a.updatedAt === b.updatedAt;
};
const OptimizedComponent = () => {
const item = useStore(
(state) => state.currentItem,
customEqual
);
return <div>{item.title}</div>;
};
複雑なデータ構造での最適化
ネストした複雑なデータ構造を効率的に扱う方法です。
typescript// 複雑なデータ構造の最適化
const ComplexDataComponent = () => {
const viewData = useStore(
(state) => ({
// 必要な部分のみを抽出
userBasic: {
id: state.user.id,
name: state.user.name,
avatar: state.user.profile.avatar,
},
notifications: state.notifications
.filter((n) => !n.read)
.slice(0, 5)
.map((n) => ({
id: n.id,
message: n.message,
type: n.type,
})),
settings: {
theme: state.ui.theme,
language: state.ui.language,
},
}),
shallow
);
return (
<div>
<UserHeader user={viewData.userBasic} />
<NotificationList
notifications={viewData.notifications}
/>
<SettingsPanel settings={viewData.settings} />
</div>
);
};
パフォーマンス測定と検証方法
React DevTools での測定
React DevTools の Profiler を使用した具体的な測定方法を説明します。
typescript// プロファイリング用のラッパーコンポーネント
const ProfiledComponent = ({
children,
id,
}: {
children: React.ReactNode;
id: string;
}) => {
return (
<Profiler
id={id}
onRender={(id, phase, actualDuration) => {
if (actualDuration > 16) {
console.warn(
`${id}: Slow render in ${phase} phase (${actualDuration}ms)`
);
}
}}
>
{children}
</Profiler>
);
};
// 使用例
const App = () => (
<div>
<ProfiledComponent id='UserProfile'>
<UserProfile />
</ProfiledComponent>
<ProfiledComponent id='PostList'>
<PostList />
</ProfiledComponent>
</div>
);
カスタムパフォーマンス測定フック
より詳細な測定のためのカスタムフックを実装します。
typescript// パフォーマンス測定フック
const usePerformanceTracker = (componentName: string) => {
const renderCount = useRef(0);
const totalRenderTime = useRef(0);
const startTime = useRef(0);
// レンダリング開始
startTime.current = performance.now();
useEffect(() => {
const endTime = performance.now();
const renderTime = endTime - startTime.current;
renderCount.current += 1;
totalRenderTime.current += renderTime;
const avgRenderTime =
totalRenderTime.current / renderCount.current;
console.log(`${componentName} Performance:`, {
renderCount: renderCount.current,
lastRenderTime: renderTime.toFixed(2),
avgRenderTime: avgRenderTime.toFixed(2),
});
});
return {
renderCount: renderCount.current,
avgRenderTime:
totalRenderTime.current /
Math.max(renderCount.current, 1),
};
};
A/B テストによる比較
最適化前後の性能を比較するための A/B テスト実装です。
typescript// 最適化前のコンポーネント
const UnoptimizedComponent = () => {
const store = useStore(); // 全体を取得
const perf = usePerformanceTracker('Unoptimized');
return (
<div>
<p>{store.user.name}</p>
<small>Renders: {perf.renderCount}</small>
</div>
);
};
// 最適化後のコンポーネント
const OptimizedComponent = () => {
const userName = useStore((state) => state.user.name); // 必要な部分のみ
const perf = usePerformanceTracker('Optimized');
return (
<div>
<p>{userName}</p>
<small>Renders: {perf.renderCount}</small>
</div>
);
};
// 比較テスト
const PerformanceComparison = () => {
const [showOptimized, setShowOptimized] = useState(false);
return (
<div>
<button
onClick={() => setShowOptimized(!showOptimized)}
>
Toggle:{' '}
{showOptimized ? 'Optimized' : 'Unoptimized'}
</button>
{showOptimized ? (
<OptimizedComponent />
) : (
<UnoptimizedComponent />
)}
</div>
);
};
メモリ使用量の監視
メモリリークの検出と監視方法です。
typescript// メモリ使用量監視
const useMemoryMonitor = () => {
useEffect(() => {
const interval = setInterval(() => {
if ('memory' in performance) {
const memory = (performance as any).memory;
console.log('Memory Usage:', {
used:
Math.round(memory.usedJSHeapSize / 1048576) +
' MB',
total:
Math.round(memory.totalJSHeapSize / 1048576) +
' MB',
limit:
Math.round(memory.jsHeapSizeLimit / 1048576) +
' MB',
});
}
}, 5000);
return () => clearInterval(interval);
}, []);
};
実際のアプリケーションでの最適化事例
大規模データリストの最適化
実際の EC サイトの商品リスト最適化事例です。
typescript// 最適化前:全商品データを取得
const ProductListBefore = () => {
const { products, filters, ui } = useStore(); // 全てを取得
const filteredProducts = products.filter((product) => {
return (
filters.category === 'all' ||
product.category === filters.category
);
});
return (
<div>
{filteredProducts.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
};
// 最適化後:必要なデータのみを効率的に取得
const ProductListAfter = () => {
const { filteredProducts, isLoading } = useStore(
(state) => ({
filteredProducts: state.products
.filter(
(product) =>
state.filters.category === 'all' ||
product.category === state.filters.category
)
.map((product) => ({
id: product.id,
name: product.name,
price: product.price,
image: product.image,
rating: product.rating,
})),
isLoading: state.ui.isLoading,
}),
shallow
);
if (isLoading) return <LoadingSpinner />;
return (
<div>
{filteredProducts.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
};
リアルタイムチャットアプリの最適化
チャットアプリケーションでの最適化実装例です。
typescript// チャットストアの定義
interface ChatState {
rooms: Room[];
currentRoomId: string;
messages: { [roomId: string]: Message[] };
users: User[];
ui: {
isTyping: { [userId: string]: boolean };
unreadCounts: { [roomId: string]: number };
};
}
// 最適化されたチャットルームコンポーネント
const ChatRoom = ({ roomId }: { roomId: string }) => {
const roomData = useStore(
(state) => ({
messages: state.messages[roomId] || [],
room: state.rooms.find((r) => r.id === roomId),
typingUsers: Object.entries(state.ui.isTyping)
.filter(([userId, isTyping]) => isTyping)
.map(([userId]) =>
state.users.find((u) => u.id === userId)
)
.filter(Boolean),
}),
shallow
);
return (
<div className='chat-room'>
<ChatHeader room={roomData.room} />
<MessageList messages={roomData.messages} />
<TypingIndicator users={roomData.typingUsers} />
<MessageInput roomId={roomId} />
</div>
);
};
// メッセージリストの最適化
const MessageList = ({
messages,
}: {
messages: Message[];
}) => {
const displayMessages = useMemo(
() => messages.slice(-50), // 最新50件のみ表示
[messages]
);
return (
<div className='message-list'>
{displayMessages.map((message) => (
<MessageItem key={message.id} message={message} />
))}
</div>
);
};
ダッシュボードアプリケーションの最適化
管理画面ダッシュボードでの実装例です。
typescript// ダッシュボードストア
interface DashboardState {
analytics: {
visitors: number[];
sales: number[];
conversions: number[];
};
widgets: Widget[];
user: User;
notifications: Notification[];
}
// 最適化されたダッシュボードコンポーネント
const Dashboard = () => {
return (
<div className='dashboard'>
<DashboardHeader />
<div className='dashboard-grid'>
<AnalyticsWidget />
<SalesWidget />
<NotificationsWidget />
<UserActivityWidget />
</div>
</div>
);
};
// 各ウィジェットは独立して最適化
const AnalyticsWidget = () => {
const analytics = useStore((state) => state.analytics);
const chartData = useMemo(
() => ({
visitors: analytics.visitors.slice(-30),
sales: analytics.sales.slice(-30),
conversions: analytics.conversions.slice(-30),
}),
[analytics]
);
return (
<div className='widget analytics-widget'>
<h3>Analytics</h3>
<Chart data={chartData} />
</div>
);
};
const NotificationsWidget = () => {
const { unreadNotifications, totalCount } = useStore(
(state) => ({
unreadNotifications: state.notifications
.filter((n) => !n.read)
.slice(0, 5),
totalCount: state.notifications.filter((n) => !n.read)
.length,
}),
shallow
);
return (
<div className='widget notifications-widget'>
<h3>Notifications ({totalCount})</h3>
<NotificationList
notifications={unreadNotifications}
/>
</div>
);
};
パフォーマンス改善の結果
実際の最適化による改善結果の例です。
| 項目 | 最適化前 | 最適化後 | 改善率 |
| ---- | -------------------------- | -------- | ------ | ------- |
| #1
| 初回レンダリング時間 | 245ms | 89ms | 64%向上 |
| #2
| 平均再レンダリング時間 | 23ms | 8ms | 65%向上 |
| #3
| 1 分間の再レンダリング回数 | 156 回 | 34 回 | 78%削減 |
| #4
| メモリ使用量 | 45MB | 32MB | 29%削減 |
| #5
| バンドルサイズ | 2.3MB | 2.1MB | 9%削減 |
これらの最適化により、ユーザー体験が大幅に向上し、特にモバイルデバイスでの動作が滑らかになりました。
まとめ
Zustand における再レンダリング最適化は、適切な selector の使用と shallow 比較の活用により実現できます。重要なポイントをまとめると以下の通りです。
selector 活用のベストプラクティス
- 必要な部分のみを取得する selector を作成する
- 計算コストの高い処理はメモ化を検討する
- 条件付き selector で動的な最適化を行う
shallow 比較の効果的な使用
- オブジェクトや配列を返す selector では積極的に活用する
- ネストした構造では限界があることを理解する
- カスタム比較関数も選択肢として検討する
パフォーマンス測定の重要性
- React DevTools を活用した定期的な測定
- カスタムフックによる詳細な分析
- A/B テストによる最適化効果の検証
実装時の注意点
- 過度な最適化は避け、実際の問題を解決する
- メモリリークに注意してクリーンアップを適切に行う
- チーム全体でのパフォーマンス意識の共有
適切な最適化により、Zustand を使用したアプリケーションのパフォーマンスを大幅に向上させることができます。ユーザー体験の向上と開発効率の両立を目指して、継続的な改善を行っていきましょう。
関連リンク
- article
ユーザー体験を劇的に変える!React で実装するマイクロインタラクション事例集
- article
CSS だけじゃ物足りない!React アプリを動かす JS アニメーション 10 選
- article
React × Three.js で実現するインタラクティブ 3D アニメーション最前線
- article
htmx と React/Vue/Svelte の違いを徹底比較
- article
あなたの Jotai アプリ、遅くない?React DevTools と debugLabel でボトルネックを特定し、最適化する手順
- article
ReactのJSX をそのまま使える!SolidJS の独自構文を体感しよう
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
- review
人類はなぜ地球を支配できた?『サピエンス全史 上巻』ユヴァル・ノア・ハラリが解き明かす驚愕の真実
- review
え?世界はこんなに良くなってた!『FACTFULNESS』ハンス・ロスリングが暴く 10 の思い込みの正体