お疲れ様です。SJC共同開発推進室の境田です。
地図画面のパフォーマンス問題、「重い」「カクつく」「固まる」といった経験はありませんか?
私も同じ課題に直面し、当初は地図画面の最適化(useRef、クラスター表示、ルート表示の一本化など)に注力しましたが、改善は見られませんでした。
本当の原因は地図ではなく、サイドパネルでの大量画像表示とそれに伴う状態の密結合にあったのです。
今回は、この根本原因を特定し、仮想化ライブラリ react-window を導入するだけで劇的なパフォーマンス改善を実現した経験を、具体的なコード例と共に紹介します。
同様の悩みを抱える方の解決の糸口になれば幸いです。
背景:地図画面が重くなっていた本当の原因
地図アプリのサイドパネル(RightMenu)で EventImage
を大量に表示していたところ、次のような症状が発生しました。
- 地図スクロールやズームがもたつく
- 画像の読み込みに合わせてUIが固まる
- リスト項目が増えるほど全体の動作が重くなる
一見、RightMenuは地図とは無関係のように見えますが、以下のような密結合した設計が原因でした。
- 画像の
onLoad
/onError
で地図の状態を更新していた RightMenu
とMapManager
が同じselectedEvent
やevents
を状態として共有していた- 非仮想化のため、見えていない画像までDOMに描画され、全ての画像リクエストが走っていた
このような構成では、画像の読み込みが連続して発生することで、Reactの再レンダリングが地図側まで波及し、結果としてUIスレッドが詰まってしまう状態になります。
導入した解決策:仮想化ライブラリ react-window
Reactの仮想化ライブラリ react-window
とは?
Reactアプリケーションで長大なリストやグリッドを効率的にレンダリングするためのライブラリです。Webページ上で数千、数万といった要素を一度に表示しようとすると、DOM要素の描画やイベントリスナーの管理にかかるコストが膨大になり、パフォーマンスが著しく低下します。これを解決するのが「仮想化(Virtualization)」という手法です。
仮想化は、「画面に表示されている要素のみを実際にDOMに描画し、スクロールに合わせて動的に要素を入れ替える」 ことで、描画コストを劇的に削減します。これにより、大量のデータがあっても、あたかもすべてが描画されているかのようにスムーズなUIを実現できます。
react-window
は、その仮想化をシンプルなAPIで実現してくれるライブラリです。類似のライブラリに react-virtualized
がありますが、react-window
はより軽量で、特定のユースケースに特化しているため、学習コストも低く導入しやすいのが特徴です。
react-window
の主な特徴
- 軽量で高速: 必要最低限の機能に絞り込まれているため、バンドルサイズが小さく、高速に動作します。
- シンプルなAPI:
FixedSizeList
やVariableSizeList
といったコンポーネントを提供するシンプルなAPIで、簡単に仮想化リストを実装できます。 - 固定サイズ・可変サイズの両方に対応: リスト内のアイテムの高さが固定の場合(
FixedSizeList
)と、動的に変わる場合(VariableSizeList
)の両方に対応しています。 - 柔軟なカスタマイズ性: レンダリングされる各アイテムのスタイルや内容を細かく制御できます。
react-window
のユースケース
react-window
は、以下のようなシナリオで特に効果を発揮します。
- チャットアプリケーションのメッセージ履歴: 数千、数万に及ぶメッセージをスムーズにスクロールさせたい場合。
- SNSのタイムライン: 無限スクロールで多数の投稿を表示する場合。
- データテーブル: 大量の行を持つ表データを扱う場合。
- 画像ギャラリー: 多数の画像をサムネイル表示するようなケース。
- 今回の地図アプリのサイドパネル: 大量のイベント画像をリスト表示し、パフォーマンスを向上させたい場合。
react-window
の導入による解決
今回のケースでは、Reactの仮想化ライブラリ react-window
を使い、RightMenuのイベントリストを仮想化しました。これにより、次のような劇的な改善が得られました。
- 描画と画像リクエストが「画面に見えている分」だけに限定され、描画コストが大幅に削減されました。
- 地図側のレンダリングに影響を与えなくなり、スクロールやズーム操作も非常にスムーズになりました。
- イベント件数が1000件以上でも、表示の重さを全く感じなくなりました。
実装手順
1. react-window
をインストール
まずはプロジェクトに react-window
を追加します。
1 2 |
npm install react-window |
2. RightMenuList.tsx
を作成
react-window
の FixedSizeList
コンポーネントを使用して、リストのレンダリングを行います。FixedSizeList
は、各リストアイテムの高さが固定の場合に最適です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import { FixedSizeList as List } from 'react-window'; import Item from './Item'; function RightMenuList({ events }) { return ( <List height={600} // メニューの高さに合わせて調整 itemCount={events.length} itemSize={220} // 1アイテムの高さ(px)に合わせて調整 width={'100%'} > {({ index, style }) => ( <div style={style}> <Item event={events[index]} /> </div> )} </List> ); } export default RightMenuList; |
補足: 上記の itemHeight
は、RightMenuList
コンポーネントを使う側から渡すことを想定しています。これは、リスト内の各アイテムの高さが固定である必要があるためです。実際のアプリケーションに合わせて適切な高さを設定してください。
3. RightMenuのリスト描画を差し替え
既存の map
関数によるリスト描画を、新しく作成した RightMenuList
コンポーネントに置き換えます。
1 2 3 4 5 |
// 旧: events.map(event => <Item event={event} ... />) // 新: <RightMenuList events={events} /> |
上記コード例のitemHeight
の箇所は、ご自身のアプリケーションの各リストアイテムの高さに合わせて修正してください。(例: itemHeight={150}
)
結果:仮想化だけでパフォーマンスが大幅改善
この react-window
の導入により、期待をはるかに超えるパフォーマンス改善が実現しました。
- UIスレッドがブロックされず、地図操作が驚くほどスムーズになりました。
- DOMノード数と画像リクエスト数が激減し、ブラウザの負荷が大幅に軽減されました。
onLoad
/onError
による副作用も排除でき、地図の不要な再描画が不要になったため、状態管理もシンプルになりました。
まとめ
今回の地図アプリのパフォーマンス改善は、以下のような重要な学びを与えてくれました。
- 地図の重さの本質的な原因は、表面的な地図の描画ではなく、「状態の密結合」と「大量の同期的画像描画」というサイドパネル側の問題にありました。
react-window
のような仮想化ライブラリを導入することで、必要な分だけDOMに描画され、描画と状態の責務分離が効果的に実現できました。- 特別なプリフェッチや複雑なキャッシュ戦略を使わなくても、仮想化というシンプルかつ強力な手法だけで、十分に劇的なパフォーマンス改善が可能です。
おわりに
今回の地図アプリのパフォーマンス改善は、一見複雑に見える問題の根本原因が、意外な場所にあることを改めて教えてくれました。
そして、react-window
のようなシンプルかつ強力なツールが、いかに大きな効果をもたらすかを示す良い例となりました。
この経験を通じて、表面的な現象だけでなく、その裏に隠された設計や状態管理の重要性を深く認識することができました。
パフォーマンス改善は、まさに「神は細部に宿る」という言葉を体現していると感じています。
最後まで閲覧ありがとうございます。
また、エコモットでは、ともに未来の常識を創る仲間を募集しています。
弊社に少しでも興味がある方はぜひ下記の採用ページをご覧ください!