大量リストを高速化する Vue 3 管理UI パフォーマンスチェックリスト
仮想化、デバウンス検索、コンポーネントのメモ化、安定した読み込み状態で重いリストを高速化するための Vue 3 管理 UI パフォーマンスチェックリスト。

なぜ重い管理リストは遅く感じるのか
ユーザーが言うのは大抵「このコンポーネントは非効率だ」ということではありません。画面がもたつくと感じます:スクロールが引っかかる、入力が遅れる、クリックの反応がワンテンポ遅い。データが正しくても、その遅れがあると人はためらい、ツールを信頼しなくなります。
管理 UI はリストが「単なるリスト」ではないためすぐに重くなります。単一のテーブルに何千行もの行、たくさんの列、バッジ、メニュー、アバター、ツールチップ、インライン編集を含むことがあります。ソート、複数フィルタ、ライブ検索を追加すると、ページは小さな変更ごとに大きな仕事をするようになります。
人が最初に気づくのは単純なことです:スクロールでフレームが落ちる、検索が指の動きに追いつかない、行メニューが遅れて開く、まとめ選択でフリーズする、読み込み状態が点滅したりページがリセットされる。
内部で起きているパターンも単純です:あまりに多くのものが頻繁に再レンダーされる。キー入力がフィルタを引き起こし、フィルタがテーブル更新を呼び、すべての行がセルを再構築します。各行が安価なら何とかやり過ごせますが、各行が小さなアプリに近いなら、毎回コストを支払うことになります。
Vue 3 管理 UI のパフォーマンスチェックリストはベンチマークに勝つためのものではありません。入力の滑らかさ、スクロールの安定、クリックの反応性、そしてユーザーの邪魔をしない進捗の可視化を保つためのものです。
良いニュースは、小さな変更が大きな書き直しに勝つことが多い点です。レンダリングする行を減らす(仮想化)、キー入力ごとの処理を減らす(デバウンス)、高コストなセルの再実行を防ぐ(メモ化)、ページがジャンプしない読み込み状態を設計する——これだけで改善が見えます。
何かを変える前に計測する
ベースラインなしで調整すると、間違ったものを「直してしまう」ことが簡単に起こります。遅い管理画面を一つ選び(ユーザー一覧、チケットキュー、注文一覧など)、体感できる目標を定義してください:スクロールが滑らかで検索入力にラグがない、など。
まず遅さを再現し、それからプロファイルを取ります。
ブラウザの Performance パネルで短いセッションを記録してください:リストを読み込み、数秒間激しくスクロールし、その後検索に文字を打ちます。メインスレッドでの長いタスクや、新しい動作がないはずなのに繰り返し発生するレイアウト/ペイント作業を探します。
次に Vue Devtools を開き、実際に何が再レンダーされているかを確認します。もし1つのキー入力でテーブル全体、フィルタ、ページヘッダーが再レンダーされるなら、それが入力遅延の原因であることが多いです。
後で改善を確認できるようにいくつかの指標を追跡します:
- 最初に使えるリストが表示されるまでの時間(単なるスピナーではない)
- スクロールの体感(滑らか/断続的)
- 入力時の遅延(文字は即座に表示されるか)
- テーブルコンポーネントのレンダリング時間
- リスト API 呼び出しのネットワーク時間
最後にボトルネックがどこにあるかを確認します。簡単なテストはネットワークノイズを減らすことです。キャッシュされたデータでも UI がもたつくなら主にレンダリングが原因です。UI は滑らかでも結果の到着が遅いならネットワーク時間、クエリサイズ、サーバ側フィルタに注力してください。
大きなリストやテーブルは仮想化する
仮想化は、管理画面で数百〜数千行を一度にレンダリングする場合、最も大きな勝利になることが多いです。すべての行を DOM に置く代わりに、ビューポートに見えているものだけ(小さなバッファを含む)をレンダリングします。これによりレンダリング時間が短くなり、メモリ使用量が下がり、スクロールが安定します。
適切なアプローチを選ぶ
長いデータセットを滑らかにスクロールする必要があるなら仮想スクロール(ウィンドウイング)が最適です。ページ単位で移動する場面やサーバ側クエリを単純にしたい場合はページネーションが良いです。コントロールを少なくしつつ巨大な DOM を避けたい場合は「さらに読み込む」パターンも有効です。
大まかな目安:
- 0〜200 行:通常レンダリングで問題ないことが多い
- 200〜2,000 行:UX に応じて仮想化かページネーション
- 2,000 行以上:仮想化+サーバ側フィルタ/ソート
仮想化を安定させる
仮想リストは各行の高さが予測可能なときに最も効果的です。レンダリング後に行高さが変わると(画像の読み込み、テキストの折り返し、展開セクションなど)、スクローラーは再測定をしなければならず、スクロールがジャンプしたりレイアウトが乱れます。
安定させる方法:
- 可能なら固定行高、または限られた既知の高さのセットを使う
- 可変コンテンツ(タグ、メモ)はクランプして詳細ビューで展開する
- 各行に強力でユニークなキーを使う(配列インデックスは決して使わない)
- スティッキーヘッダーは仮想化された本文の外に置く
- どうしても可変高さをサポートするなら測定を有効にし、セルをシンプルに保つ
例:チケットテーブルに 10,000 行があるなら、テーブル本文を仮想化して行高さを一貫させ(ステータス、件名、担当者)、長文メッセージは詳細ドロワーの背後に置いてスクロールを滑らかにします。
デバウンス検索と賢いフィルタリング
検索ボックスは高速なテーブルを遅く感じさせることがあります。問題は通常フィルタ自体ではなく、その連鎖反応です:各キー入力がレンダーやウォッチャー、しばしばリクエストを引き起こします。
デバウンスは「ユーザーが入力を止めてから少し待ち、一度だけ処理する」ことを意味します。多くの管理画面では 200〜400 ms が、落ち着いていて反応も良い適切な範囲です。空白のトリムや 2〜3 文字未満の検索を無視することも、データに合えば検討してください。
フィルタリング戦略はデータセットのサイズとルールに合わせるべきです:
- 数百行以下で全て読み込んでいるならクライアント側フィルタで十分
- 数千行ある、または権限が厳しい場合はサーバに問合せを投げる
- フィルタが高コスト(期間指定、複雑なステータスロジック)ならサーバ側へ押し出す
- 両方必要なら混合アプローチ:素早いクライアント側の絞り込み→最終結果はサーバで取得
サーバ呼び出し時は古い結果への対処をしてください。たとえばユーザーが「inv」と入力して素早く「invoice」と続けた場合、先に出た要求が後から返ってきて UI を上書きしてしまう可能性があります。以前のリクエストをキャンセルする(fetch の AbortController や HTTP クライアントのキャンセル機能)、またはリクエスト ID を追跡して最新以外を無視する方法があります。
読み込み状態は速度と同じくらい重要です。キー入力ごとに全画面スピナーを出すのは避けましょう。落ち着いたフローの例:ユーザーが入力中は何もチカチカさせない。アプリが検索中は入力近辺に小さなインラインのインジケータを表示。結果更新時は「42 件を表示中」のような控えめで明確な表示。結果がないときは「一致なし」と表示して空のグリッドを放置しない。
メモ化コンポーネントと安定したレンダリング
遅い管理テーブルの多くは「データが多いから」遅いのではなく、同じセルが何度も再レンダーされるため遅くなっています。
何が再レンダーを引き起こすかを見つける
繰り返し更新が発生する一般的な習慣は次の通りです:
- 表示に必要なフィールドだけではなく大きなリアクティブオブジェクトを props に渡している
- テンプレート内でインライン関数を作成している(レンダーごとに新しくなる)
- 大きな配列や行オブジェクトに対する deep watcher を使っている
- 各セルで新しい配列やオブジェクトをテンプレート内で作っている
- 各更新でフォーマット処理(日時、通貨、パース)をしている
props やハンドラのアイデンティティが変わると、Vue は子が更新を必要とする可能性があると見なします。見た目に変化がなくても更新されてしまうのです。
props を安定させ、メモ化する
まずは小さく安定した props を渡すことから始めます。各セルに row オブジェクト全体を渡すのではなく、row.id とそのセルが表示する特定フィールドだけを渡すようにしましょう。派生値は computed に移し、入力が変わったときだけ再計算されるようにします。
行の一部がめったに変わらない場合は v-memo が役立ちます。安定した入力(例えば row.id と row.status)に基づいて静的部分をメモ化すれば、検索中の入力やホバーがすべてのセルの再実行を強制するのを防げます。
また高コストな処理をレンダーパスから外してください。日時を一度だけ整形する(例えば id をキーにした computed マップで保持)、あるいはサーバで整形して返すのが理にかなう場合もあります。一般的な勝ち筋は、何百行にもわたって new Date() を毎回呼ぶのをやめることです。
目標は単純です:アイデンティティを安定させ、テンプレート内の作業を減らし、本当に変わった部分だけを更新すること。
体感が速くなる賢い読み込み状態
リストが実際より遅く感じられるのは UI が頻繁にジャンプするためです。良い読み込み状態は待ち時間を予測可能にします。
スケルトン行はデータの形が分かっている(テーブル、カード、タイムライン)場合に有効です。スピナーは何を待っているかを伝えません。スケルトンは何行分か、アクションはどこに出るか、レイアウトはどうなるかを示して期待を設定します。
データをリフレッシュする(ページング、ソート、フィルタ)ときは、新しいリクエスト中も前の結果を画面に残してください。小さな「更新中」ヒントを表示してテーブルを消さないことで、ユーザーは更新中も読み続けたり確認したりできます。
部分的な読み込みは全体のブロックより優れる
すべてを凍結させる必要はありません。テーブル読み込み中もフィルタバーは表示したまま一時的に無効にする。行アクションに追加データが必要なら、クリックした行だけに保留状態を表示してページ全体をブロックしないようにする。
安定したパターン例:
- 最初の読み込み:スケルトン行
- リフレッシュ:古い行を表示したまま小さな「更新中」ヒントを表示
- フィルタ:フェッチ中は無効にするがレイアウトは動かさない
- 行アクション:行単位の保留状態
- エラー:レイアウトを崩さないインライン表示
レイアウトシフトを防ぐ
ツールバー、空状態、ページネーションのスペースを確保して、結果が変わったときにコントロールが動かないようにします。テーブル領域の固定 min-height を設定し、ヘッダー/フィルタバーを常にレンダリングしておくとページのジャンプを避けられます。
具体例:チケット画面で「Open」から「Solved」に切り替えてもリストを真っ白にしないでください。現在の行を残し、状態フィルタを一時的に無効化し、更新されたチケットのところだけ保留状態にします。
ステップバイステップ:午後だけで遅いリストを直す
遅い画面を一つ選び、小さな修理と考えて取り組んでください。目標は完璧ではなく、スクロールと入力で明確に体感できる改善を出すことです。
簡単な午後プラン
まず痛みの正体を名付けます。ページを開いて三つのことを試してください:速くスクロールする、検索ボックスに文字を打つ、ページやフィルタを変更する。多くの場合、本当に壊れているのはそのうちの一つだけで、それが最初に直すべきポイントを示します。
その後、シンプルな順序で作業します:
- ボトルネックを特定:ジャンクなスクロール、遅い入力、遅いネットワーク応答、または混合
- DOM サイズを削る:仮想化、または UI が安定するまでデフォルトのページサイズを小さくする
- 検索を落ち着かせる:入力をデバウンスし、古いリクエストをキャンセルして結果の順序を守る
- 行を安定させる:一貫したキー、テンプレート内で新しいオブジェクトを作らない、行レンダリングをメモ化する
- 体感速度を上げる:行単位のスケルトンや小さなインラインスピナーでページ全体をブロックしない
各ステップの後で、最初に違和感があったアクションを再テストします。仮想化でスクロールが滑らかになれば次へ、入力がまだ遅いならデバウンスとリクエストキャンセルが次の大きな勝ち筋です。
コピーできる小さな例
「Users」テーブルに 10,000 行あるとします。スクロールがカクつくのはブラウザがあまりにも多くの行をペイントしているためです。表示されている行だけレンダリングするように仮想化します。
次に検索が遅く感じる原因は毎キーでリクエストが飛ぶことです。250〜400 ms のデバウンスを追加し、AbortController(または HTTP クライアントのキャンセル機能)で前のリクエストをキャンセルして、最新のクエリだけがリストを更新するようにします。
最後に各行を安価に保ちます。props はシンプルに(可能な限り id とプリミティブ)、行の出力をメモ化して関係ない更新で再描画されないようにし、全画面オーバーレイではなくテーブル内に読み込みを表示してページの応答性を維持します。
よくある誤り
チームはしばしばいくつかの修正を適用して小さな改善を見て満足し、その後停滞します。理由は大抵、重い部分が「リストそのもの」ではなく、各行がレンダリング・更新・データ取得時にする仕事にあるからです。
仮想化は助けになりますが、簡単に相殺されます。もし可視行ごとに重いチャートをマウントしたり、画像をデコードしたり、ウォッチャーを大量に走らせたり、高価なフォーマットを行っているとスクロールはまだつらくなります。仮想化は表示される行数を制限するだけで、各行が重いことを防ぐわけではありません。
キーも静かなパフォーマンスキラーです。配列インデックスをキーに使うと、挿入・削除・ソート時に Vue は行を正しく追跡できません。これが再マウントを強制し、入力フォーカスをリセットすることがあります。安定した id を使って DOM とコンポーネントインスタンスを再利用してください。
デバウンスもやりすぎると裏目に出ます。遅すぎるとユーザーは何も起きないと感じ、結果が飛んでくるまで待たされる印象になります。短めの遅延が多くの場合うまく働きますし、「Searching...」のような即時フィードバックを出すことでアプリが受け付けていることを示せます。
よく見かける五つのミス:
- リストを仮想化しても、可視行に重いセル(画像、チャート、複雑なコンポーネント)を置いたままにする
- ソートや更新時に配列インデックスをキーにして行が再マウントされる
- デバウンス遅延が長すぎて UI が遅く感じる
- 幅広いリアクティブ変更からリクエストを発生させる(フィルタ全体を deep watch する、URL 状態を頻繁に同期する)
- スクロール位置を消し、フォーカスを奪うグローバルなページローダーを使う
もし Vue 3 管理 UI のパフォーマンスチェックリストを使うなら、"何が再レンダーするか" と "何が再取得するか" を第一級の問題として扱ってください。
クイックパフォーマンスチェックリスト
テーブルやリストがもたつき始めたときに使ってください。目標は滑らかなスクロール、予測可能な検索、そして驚きの少ない再レンダリングです。
レンダリングとスクロール
多くの「遅いリスト」問題は、レンダリングしすぎ・頻繁すぎに起因します。
- 画面に数百行を表示できるなら仮想化を使い、DOM には画面上の要素だけ(小さなバッファ含む)を入れる
- 行高さは安定させる。可変高さは仮想化を壊しジャンクを生む
- インラインで新しいオブジェクトや配列を props に渡すのは避ける(例:
:style="{...}")。一度作って再利用する - 行データに対する deep watcher は注意。
computedと特定フィールドに対する限定的な watch を優先する - 配列インデックスではなく、実際のレコード id に合う安定したキーを使う
検索、読み込み、リクエスト
ネットワークが遅くてもリストが速く感じるようにする
- 検索は 250〜400 ms 前後でデバウンスし、入力にフォーカスを保ち、古いリクエストをキャンセルして古い結果が上書きしないようにする
- 新しい結果を読み込む間は既存の結果を表示したままにし、小さな「更新中」状態を使う
- ページネーションは予測可能に(固定ページサイズ、明確な次/前の挙動、驚きのリセットなし)
- 関連する呼び出しはまとめてバッチ処理するか並列にフェッチし、まとめてレンダリングする
- 同じフィルタセットの最後の成功応答をキャッシュして、戻ったときにすぐ表示できるようにする
例:負荷のかかるチケッティング管理画面
サポートチームが一日中開いたままにするチケット画面があります。顧客名、タグ、注文番号で検索しつつ、ライブフィードがチケットステータスを更新します(新しい返信、優先度変更、SLA タイマー)。テーブルは簡単に 10,000 行に到達します。
最初のバージョンは動作しますが体感がひどい。入力した文字が遅れて表示され、テーブルは上にジャンプしてスクロール位置がリセットされ、アプリはキー入力ごとにリクエストを送ります。結果が古いものと新しいものの間でチカチカします。
改善した点:
- 検索入力をデバウンス(250〜400 ms)してユーザーが止まったときだけ問い合わせるようにした
- 新しいリクエスト中も前の結果を表示し、フリッカーを止めた
- 行を仮想化して DOM に表示される行だけをレンダリングした
- チケット行をメモ化して無関係なライブ更新で再レンダーされないようにした
- 重いセルコンテンツ(アバター、リッチスニペット、ツールチップ)は行が可視になったときに遅延読み込みした
デバウンス後、入力遅延は消え、無駄なリクエストが減った。前の結果を残すことでフリッカーが止まり、ネットワークが遅くても画面は安定して見えた。
仮想化が最大の視覚的勝利で、ブラウザが大量の行を同時に扱う必要がなくなったためスクロールが滑らかになった。行のメモ化は、単一チケットの変更でテーブル全体が更新されるのを防いだ。
さらに、ライブフィードの更新を数百ミリ秒ごとにバッチ適用することで UI の再フローを減らした。
結果:スクロールが安定し、入力が速く、驚きが減った。
次のステップ:パフォーマンスをデフォルトにする
高速な管理 UI は後から救うより維持する方が簡単です。本チェックリストを新しい画面ごとの標準にしてください。大きな改善は通常、ブラウザが描画するものを減らし、入力に対する反応を速くすることから生まれます。
基本から始めましょう:DOM サイズを減らす(長いリストを仮想化、非表示行をレンダリングしない)、入力遅延を減らす(検索をデバウンス、重いフィルタをキー入力ごとに実行しない)、レンダリングを安定させる(行コンポーネントをメモ化、props を安定化)。小さなリファクタは最後に回してください。
その後、回帰を防ぐためのガードレールを導入します。例えば、200 行を超えるリストは仮想化、検索入力はデバウンス、すべての行は安定した id キーを使う、など。
再利用可能なビルディングブロックがあれば容易になります。デフォルトの設定が入った仮想テーブルコンポーネント、デバウンス済み検索バー、テーブルレイアウトに合ったスケルトン/空状態を用意すれば、Wiki よりも効果的です。
実用的な習慣として:新しい管理画面をマージする前に、データを10倍にして遅ネットワークプリセットでテストしてください。それでも体感が良ければ実運用でも優れた体験になります。
もし内部ツールを素早く構築し、これらのパターンを画面全体で一貫させたいなら AppMaster (appmaster.io) は適した選択肢になり得ます。AppMaster は実際の Vue 3 Web アプリを生成するため、リストが重くなったときも同じプロファイリングと最適化のアプローチが適用できます。
よくある質問
レンダリングが数百行を超えるなら、まずは仮想化を導入してください。スクロール中にブラウザが何千もの DOM ノードを扱う必要がなくなるため、体感で最も大きな改善が得られることが多いです。
スクロールでフレームが落ちるならレンダリング/DOM の問題であることが多いです。UI は滑らかでも結果の到着が遅いならネットワークやサーバ側のフィルタリングが原因です。キャッシュ済みデータや高速ローカル応答で確認すると判別できます。
仮想化は、表示されている行(と小さなバッファ)だけをレンダリングし、データセット全体を DOM に置かなくなる仕組みです。これにより DOM サイズ、メモリ使用、スクロール時の処理量が減ります。
行高さが一貫していることを目指してください。レンダリング後にサイズが変わるコンテンツ(行が展開する、画像が読み込まれる、テキストが折り返されるなど)があると、スクローラーが再測定を行いジャンプやジャンクが発生します。
管理画面の検索では 250〜400 ms 程度が良い出発点です。短すぎると毎キーで再フィルタが走り、長すぎると入力体験が遅く感じます。データに合えば空白トリムや 2〜3 文字未満の検索無視も検討してください。
前のリクエストをキャンセルするか古いレスポンスを無視してください。目標は単純で、最新のクエリだけがテーブルを更新できるようにすることです。AbortController(fetch)やクライアントのキャンセル機能を使いましょう。
必要以上に大きなリアクティブオブジェクトを渡したり、テンプレート内で新しいインライン関数やオブジェクトを作成したりすることが原因です。プロパティやハンドラのアイデンティティを安定させ、変化しない部分は v-memo のようなメモ化を使いましょう。
レンダーパスから高価な処理を取り除いてください。フォーマット済みの値(日時や通貨)は一度だけ算出してキャッシュし、基データが変わったときにだけ更新するようにします。例えば多数の行で new Date() を毎回呼ぶのは避けましょう。
更新中でも前の結果を画面に残し、テーブルをクリアする代わりに小さな “更新中” 表示を出すと良いです。これによりフリッカーやレイアウトジャンプを防ぎ、遅いネットワークでも画面が安定して見えます。
はい。AppMaster は実際の Vue 3 Web アプリを生成するため、同じ最適化手法がそのまま適用できます。レンダリングプロファイル、仮想化、検索のデバウンス、行の安定化は全て有効です。


