ページネーション、仮想化、賢いフィルタリング、クエリ最適化を組み合わせて、100k 行のダッシュボード一覧を高速に保つ方法を解説します。社内ツールの応答性を維持する実践的な手順とチェックリスト付きです。

一覧画面は、多くの場合「普段は問題ない」状態から徐々に遅くなっていきます。スクロールが引っかかる、更新後にページが一瞬固まる、フィルタ応答に数秒かかる、クリックのたびにスピナーが出る、といった小さな遅延が積み重なって体感が悪くなります。ブラウザのタブがフリーズしたように見えることも、UI スレッドが重い処理で塞がっているのが原因です。
100k 行という規模はよく転換点になります。データ量自体はデータベースにとって普通でも、ブラウザやネットワーク、クエリの小さな非効率が一度に目に見えて出てきます。すべてを一度に表示しようとすると、単純な画面が重いパイプラインに変わります。
目標は全行をレンダリングすることではありません。ユーザーが素早く必要なものを見つけられること、つまり「適切な 50 行」「次のページ」「フィルタで絞られた狭い範囲」を手早く提示することです。
作業は大きく四つに分けると整理しやすいです。
どれか1つが高コストだと画面全体の体感が遅くなります。単純な検索ボックスが 100k 行をソートするリクエストを引き起こし、数千件のレコードを返してブラウザが全部描画するような流れになると、タイピングが重くなるのは当然です。
内部ツールを素早く作ると(Koder.ai のような v ibe-coding プラットフォームも含めて)、一覧画面はしばしば実データの増加で「デモでは動くが日常では即時性がない」ことが露呈する場所になります。
最適化を始める前に、その画面で「速い」とはどういう状態かを決めてください。多くのチームはスループット(全部読むこと)を追いがちですが、ユーザーが必要としているのは低レイテンシ(素早く何かが見えること)であることが多いです。一覧は全 100k 行を読み切らなくても、スクロールやソート、フィルタに対して素早く反応すれば十分に「瞬時」に感じられます。
実用的な目標は「最初の行が表示されるまでの時間」であって、完全読み込み時間ではありません。ユーザーは最初の 20〜50 行が素早く表示され、インタラクションが滑らかであるとページを信頼します。
変更のたびに追える少数の指標を選びましょう。
COUNT(*) や幅の広い SELECT)これらはよくある症状に直結します。スクロール時にブラウザ CPU が跳ねるならフロントエンドが行ごとにやりすぎている証拠です。スピナーで待たされるがスクロールは安定しているならバックエンドやネットワーク側が原因でしょう。リクエストは速いのに画面が固まるなら、ほぼ確実に描画やクライアント側の重い処理です。
UI をそのままにして、バックエンドが返す行数を一時的に 20 行に制限してみてください。速くなるならボトルネックはロードサイズやクエリ時間です。遅いままならレンダリングやフォーマット、行ごとのコンポーネント処理を疑いましょう。
例: 内部の Orders 画面で検索時に遅いと感じる場合、API が 5,000 行を返してブラウザ側で毎キー押下ごとにフィルタしているならタイピングは遅くなります。一方、API が未インデックスのフィルタで COUNT を実行して 2 秒かかっているなら、行が変わる前に待たされます。原因が違えば直し方も違います。
ブラウザは最初のボトルネックになることが多いです。API が速くてもページが重く感じられるのは、単純にブラウザが描画するものが多すぎるからです。第一のルールは明快です: DOM に何千行もレンダリングしない。
本格的な仮想化を入れる前でも、各行を軽量に保つことが重要です。ネストされたラッパー、アイコン、ツールチップ、セルごとの複雑な条件付きスタイルが多い行は、スクロールや更新ごとにコストを生みます。凡庸なテキスト、少数のバッジ、小さなインタラクティブ要素 1〜2 個に抑えましょう。
行の高さを安定させることは思ったより効果的です。行高さが均一だとブラウザはレイアウトを予測しやすく、スクロールが滑らかになります。可変高さ(折り返しテキスト、展開メモ、大きなアバター)は追加の計測とリフローを誘発します。詳細が必要ならサイドパネルや単一の展開エリアを検討してください。
フォーマット処理も地味にコストになります。日付や通貨、重い文字列処理は多数のセルで繰り返されると積み上がります。
見えていない値はまだ計算しない。高コストなフォーマットはキャッシュし、行が可視になったときやユーザーが行を開いたときに計算するようにします。
すぐに効果が出やすい手当:
例: 請求書テーブルで 12 列分の通貨や日付をフォーマットしているとスクロール時に引っかかります。表示行ごとにフォーマットをキャッシュし、オフスクリーンの行は遅延処理するだけで、バックエンドをいじる前に体感を即改善できます。
仮想化は、実際に見えている行だけ(上下に小さなバッファを含む)を描画する仕組みです。スクロールに合わせて同じ DOM 要素を再利用し、内部のデータだけ入れ替えます。これによりブラウザは何万もの行コンポーネントを一度に描画する必要がなくなります。
長いリスト、幅の広いテーブル、重い行(アバター、ステータスチップ、アクションメニュー、ツールチップ)では仮想化が有効です。また、ユーザーが多くスクロールして連続的に見たい場合にも適しています。
魔法ではありません。よくある落とし穴:
最も単純で堅実な方法は地味ですが有効です: 行高さを固定し、列を予測可能にし、行内のインタラクティブ要素をあまり増やさないこと。
両方を組み合わせることができます: サーバー側から取得する量をページネーション(あるいはカーソル型のロードモア)で制限し、取得したスライス内で仮想化して描画コストを下げます。
実用的なパターンは、通常のページサイズ(100〜500 行程度)をフェッチし、そのページ内を仮想化して表示、ページ移動用の明確なコントロールを提供することです。無限スクロールを使う場合は "Loaded X of Y" の表示などでユーザーが全件を見ているわけではないと分かるようにしましょう。
データが増えても使える一覧画面には、ページネーションが安全なデフォルトであることが多いです。予測可能で管理系ワークフロー(レビュー、編集、承認)に合いやすく、「このフィルタでページ 3 をエクスポートする」といった要件にも対応しやすいです。多くのチームは派手なスクロールを試した後でページネーションに戻ってきます。
無限スクロールはカジュアルな閲覧体験に向きますが、位置感覚を失いやすく、戻るボタンで同じ位置に戻らない、長時間のセッションでメモリが積み上がるなどの隠れたコストがあります。Load more ボタンはページ単位の利便性を保ちながら連続読み込みの体験を提供できます。
オフセットは古典的な page=10&size=50 型です。単純ですが、大きなテーブルでは後方ページで遅くなることがあります。データベースが多くの行をスキップする必要があるためです。また新しい行が入るとページ内の要素が移動してしまうこともあります。
キーセット(カーソル)ページネーションは「最後に見た項目の後の 50 行」といった要求で、通常は id や created_at を使います。スキップが少なく高速に保ちやすいのが利点です。
実用ルール:
ユーザーは総件数を好むことが多いですが、重いフィルタで毎回 COUNT を取るのは高コストです。よく使われるフィルタについては件数をキャッシュする、ページロード後にバックグラウンドで件数を埋める、あるいは「10,000+」のような概算で示すなどの選択肢があります。
例: Orders 画面はキーセットページネーションで結果を即表示し、ユーザーがフィルタを止めて 1 秒経ったら正確な総件数を補完するといった挙動が実用的です。
Koder.ai でこれを構築する場合、ページネーションと件数の振る舞いを早い段階で画面仕様の一部として扱い、生成されるバックエンドクエリと UI 状態が後で競合しないようにしましょう。
多くの一覧画面が遅く感じるのは、最初に幅広く開いてしまうことが原因です: すべてを読み込んでからユーザーに絞らせるのではなく、役立つ狭いデフォルトから始めましょう(例: 過去 7 日、担当: 自分、ステータス: Open)。"全期間" は明示的に選ぶオプションにすると安全です。
テキスト検索も落とし穴です。キー押下ごとにクエリを走らせるとリクエストが積み上がり UI がちらつきます。入力はデバウンスして、ユーザーが一旦止まったらクエリを送るようにし、古いリクエストはキャンセルしましょう。単純なルール: ユーザーがまだタイプしている間はサーバーに問い合わせない。
フィルタは速く感じることと明確であることが両立して初めて使いやすくなります。テーブル上部にフィルタチップを表示して、何が有効になっているか一目で分かり、ワンクリックで解除できるようにしてください。チップのラベルは生のフィールド名ではなく人間向けの表現(例: Owner: Sam ではなく owner_id=42)にしましょう。"結果が消えた" と言われる原因はたいてい見えないフィルタです。
大規模なリストを複雑にしないパターン:
保存されたビューは地味ですが有効です。毎回完璧な一時的フィルタを作るのではなく、現実のワークフローに合ったプリセットをいくつか用意することで、ワンクリックで速い結果を得られるようにします。
Koder.ai のようなチャット駆動ビルダーで内部ツールを作るなら、フィルタは付け足しではなくプロダクトフローの一部として設計してください。よくある質問から始め、デフォルトビューと保存ビューをそれに合わせて作ると効率的です。
一覧画面は詳細ページと同じデータを必要とすることは稀です。API がすべてを返すと二重のコストを払います: データベース側で余計に計算して、ブラウザへも余計なデータを送ることになります。クエリ形状(query shaping)とは、一覧が今必要とするものだけを要求する習慣です。
まずは各行を描画するために必要なカラムだけを返すことから始めましょう。多くのダッシュボードでは id、ラベル、ステータス、担当者、タイムスタンプがあれば十分です。大きなテキスト、JSON ブロブ、計算フィールドはユーザーが行を開いたときまで遅延させます。
初回描画で重い JOIN を行わないようにしてください。JOIN はインデックスを使って小さい結果を返す場合は問題ありませんが、複数テーブルを結合してそれをソートやフィルタに使うと高コストになります。ひとつのテーブルから一覧を速く取って、必要に応じて詳細をオンデマンドで読み込む、あるいは可視行だけをバッチロードするパターンが有効です。
ソートオプションは限定して、インデックスがある列でソートするようにしましょう。"何でもソート可能" は一見便利ですが、大きなデータセットでの遅いソートを強いることが多いです。created_at、updated_at、status のような予測可能でインデックス化しやすい選択肢を用意しましょう。
サーバーサイドの集計にも注意が必要です。フィルタをかけた上での COUNT(*)、幅の広い列に対する DISTINCT、総ページ数の計算は応答時間を支配することがあります。
実用的アプローチ:
COUNT や DISTINCT はオプション扱いにして、キャッシュや概算を検討するKoder.ai で内部ツールを作るなら、リストクエリは詳細クエリと別に軽量に定義しておくと、テーブルが成長しても UI が速いままです。
100k 行でも一覧を速く保ちたいなら、データベースが一回あたり処理する作業を減らす必要があります。多くの遅い一覧は「データが多すぎる」わけではなく、アクセスパターンが合っていないのです。
まずはユーザーが実際に使うフィルタとソートに合ったインデックスを作りましょう。一覧が普段 status で絞って created_at でソートするなら、その順序でインデックスを張ると良いです。そうでないとデータベースは想定以上に多くの行を走査してからソートし、コストが急増します。
効果が大きい改善:
tenant_id, status, created_at)を作るOFFSET ページングよりキーセット(カーソル)を優先する例: Orders テーブルで顧客名、ステータス、金額、日付を表示するなら、一覧では関連テーブルをすべて JOIN して注文明細やメモを取らないようにします。テーブルに必要な列だけを返し、残りはクリック時に別リクエストで読み込みます。
Koder.ai のようなプラットフォームで作るなら、生成される API エンドポイントがカーソルページネーションとフィールド選択をサポートするようにして、テーブルの成長でもデータベースの仕事量が予測可能になるようにしましょう。
一覧ページが遅い場合、すべてを書き換えるのではなく、通常の利用フローを固めてからその経路を最適化していくのが近道です。
デフォルトビューを定義する。 デフォルトのフィルタ、ソート順、表示列を決めます。デフォルトで全部を見せようとすると遅くなります。
利用に合ったページング方式を選ぶ。 ユーザーが最初の数ページを眺めることが多ければ古典的なページネーションで十分です。深いページ移動が多いか、距離に依存しないパフォーマンスが必要ならキーセットを使います(created_at と id の組み合わせなど)。
テーブル本体に仮想化を入れる。 バックエンドが速くてもブラウザが多くの行を描画すると詰まることがあります。
検索とフィルタを瞬時に感じさせる。 入力はデバウンスし、各種フィルタ状態を URL や共有の状態ストアに保つことでリロードや戻る操作、共有が安定します。最後に成功した結果をキャッシュしてテーブルが空っぽにちらつかないようにしましょう。
測定し、クエリとインデックスを調整する。 サーバー時間、DB 時間、ペイロードサイズ、描画時間をログに取り、必要な列だけを選択し、フィルタを早めに適用し、デフォルトのフィルタ+ソートに合うインデックスを追加します。
例: 100k 件のチケットを扱うサポートダッシュボードなら、デフォルトを Open、自分のチームに割当、最新順ソート、6 列表示にして、チケット id、件名、担当、ステータス、タイムスタンプだけを取得します。キーセットページネーションと仮想化を組み合わせれば、データベースと UI の双方で予測可能な挙動になります。
Koder.ai で作る場合、このプランは反復と検証のワークフローにうまくマップします。ビューを調整し、スクロールと検索をテストし、クエリを絞って画面が速く保てるまで繰り返してください。
100k 行を普通のページのように扱うことが、一番速さを失わせます。遅いダッシュボードにはいくつかの典型的な落とし穴があります。
1 つは「全部レンダして CSS で隠す」ことです。見えているのが 50 行に見えても、ブラウザは 100k の DOM ノードを生成し、計測し、スクロール時に再描画のコストを払います。長いリストが必要なら、仮想化して描画するものを減らしましょう。
検索が静かに性能を壊すこともあります。キー押下ごとにフルテーブルスキャンを誘発するケースです。フィルタがインデックスに支えられていない、検索対象カラムが多すぎる、大きなテキストフィールドに対する contains クエリを無計画に使っていると発生します。最初にユーザーが触るフィルタは、UI 上便利なだけでなく DB 側で安価に実行できるものにしましょう。
また一覧でサマリしか必要ないのにフルレコードを取得するのもよくある誤りです。通常、一覧行は 5〜12 フィールドで足ります。余計なデータを取ると DB、ネットワーク、フロントエンドの解析コストが増えます。
エクスポートや総件数の計算をメインスレッドでやったり、重いリクエストを待ってから UI を応答させるとページが固まります。エクスポートはバックグラウンドで始め、進捗を見せ、フィルタ変更ごとに総件数を毎回再計算しないようにしましょう。
最後に、多すぎるソートオプションも逆効果です。すべての列でソートを許すと大きな結果セットをメモリでソートするか、データベースに遅いプランを強いることになります。ソートは少数のインデックス化された列に絞り、デフォルトソートは実際のインデックスに合わせましょう。
簡単なチェックリスト:
一覧のパフォーマンスは一度きりの調整ではなくプロダクト機能として扱ってください。本当に速い一覧画面は、実際の人が実データでスクロールし、フィルタし、ソートしたときに速く感じるものです。
このチェックリストで正しく対処できたか確認しましょう:
簡単な現実確認: 一覧を開いて 10 秒間スクロールし、その後で一般的なフィルタ(例: Status: Open)を適用してみてください。UI が固まるなら原因は大体レンダリング(DOM 行が多すぎる)か、更新ごとに走る重いクライアント側処理(ソート、グルーピング、フォーマット)です。
次の手順(順番にやることで往復作業を減らせます):
Koder.ai(koder.ai)でこれを作るなら、まず Planning Mode で正確な一覧列、フィルタフィールド、API レスポンス形を定義してください。実験で画面が遅くなったらスナップショットからロールバックすると安全です。
目標を「すべてを読み込む」から「まず役立つ最初の行を素早く表示する」に変えてください。読み込みすべてを追うより、フィルタやソート、スクロール時に滑らかに動くことを優先します。全データを一度に読み込む必要はほとんどありません。
読み込み後やフィルタ変更後の最初の行表示時間、フィルタ/ソート反映時間、レスポンスのペイロードサイズ、遅いデータベースクエリ(特に幅の広い SELECT や COUNT(*))、ブラウザのメインスレッドのスパイクを計測しましょう。これらはユーザーが感じる「遅さ」と直結します。
簡単な実験:同じフィルタとソートで API の返す行数を一時的に 20 行に制限してみてください。速くなるならボトルネックはクエリやペイロードサイズ、速くならないならレンダリングやフォーマット、行ごとのクライアント処理が原因です。
DOM に何千行もレンダリングしない、行コンポーネントは軽くする、行高さを固定することが最も簡単で効果的です。画面外の行に対して高負荷なフォーマットを実行しないようにし、表示時や展開時に計算・キャッシュするのが有効です。
仮想化は表示できる行だけを描画し、DOM 要素を再利用してデータだけ入れ替える手法です。行高さが一定でレイアウトが予測可能な場合によく効きますが、可変高さや固定ヘッダー、キーボード操作、バルク選択には追加対応が必要です。
管理系や社内ワークフローではページネーションが無難です。無限スクロールは閲覧体験としては良い場合もありますが、位置感覚を失いやすく戻る操作やメモリ管理で問題が出がちです。中間案として "もっと読む" ボタンでページ単位を維持する手もあります。
オフセット(page=10&size=50 型)は実装が簡単ですが、大きなテーブルでは深いページで遅くなることがあります。キーセット(カーソル)ページネーションは一般に速く、挿入が多いテーブルや深いナビゲーションに向きますが、正確なページ番号へのジャンプには向きません。
毎回キーストロークでサーバーに問い合わせないこと。入力をデバウンスし、古いリクエストはキャンセルする。初回クエリは狭めのデフォルト(直近 7 日など)にしてサーバー負荷を抑えると、体感はぐっと良くなります。
一覧で実際に描画する列だけを返すこと。通常は id、ラベル、ステータス、担当者、タイムスタンプなどの小さなセットで十分です。大きなテキストや JSON、関連データは詳細要求で遅延読み込みしましょう。
デフォルトで使われるフィルタとソートに合わせてインデックスを作ること。複合インデックスでフィルタ+ソートを支援し、OFFSET 深追いは避ける。合計件数は必須扱いにせず、キャッシュや概算("10,000+")で済ませるのも有効です。