ReoGrid ReoGrid Web

ブラウザで 10,000 行を描画する — ReoGrid Web が 60fps を維持する仕組み

· unvell team
ブラウザで 10,000 行を描画する — ReoGrid Web が 60fps を維持する仕組み

ボタンをクリックすると、10,000 行のデータ — 企業、地域、売上、成長率、ステータス — がスプレッドシートに表示されます。スクロールを始めても、フレームレートは 60fps を維持。売上で並び替えても、100ms 未満で再描画されます。

これは本サイトの ビッグデータデモ で、あなたのブラウザ上でそのまま動いています。本記事では、その仕組みと、なぜそれが動くのかを解説します。


DOM は密なグリッドには不向きなツール

DOM ベースのグリッドには構造的な問題があります。可視セルはすべて <div>(あるいは <td>)であり、ブラウザのレイアウト/スタイル/ペイントのパイプラインがスクロールや値変更のすべてのクリティカルパスに乗ってきます。仮想化グリッドはこれを「可視行だけ DOM ノードを使い回す」ことで覆い隠そうとしますが、結合セル・ウィンドウ枠固定・条件付き書式・セルごとの罫線などをグリッドに組み合わせ始めると、リサイクラが同期し続けなければならない DOM ノードがレイヤーごとに増えていきます。

ReoGrid Web は単一の <canvas> 要素です。セルごとの DOM はありません。レンダラーはワークシートの可視矩形を走査し、2D コンテキストに直接描画します。スクロールはリフローではなく再描画です。10,000 行の並び替え+再描画も、総行数とは無関係に O(可視セル数) で完結します。


ビッグデータデモが実際にやっていること

big-data.example.ts から要点を抜粋したロードパスです。

import { createReogrid } from '@reogrid/pro';

const { worksheet: ws } = createReogrid({
  workspace: '#grid',
  licenseKey: 'YOUR_LICENSE_KEY',
});

ws.setGridSize(10_002, 9);            // 10,000 行 + ヘッダー + バッファ

const data: Row[] = generateRows(10_000);

function renderData(rows: Row[]) {
  ws.suspendRender();                  // ← ペイントをまとめる
  rows.forEach((d, i) => {
    const r = i + 1;
    ws.cell(r, 1).setValue(d.name);
    ws.cell(r, 4).setValue(d.revenue.toLocaleString());
    ws.cell(r, 6)
      .setValue(`${d.growth >= 0 ? '+' : ''}${d.growth.toFixed(1)}%`)
      .setStyle({ color: d.growth >= 0 ? '#16a34a' : '#dc2626', bold: Math.abs(d.growth) >= 20 });
    // …他の列
  });
  ws.resumeRender();                   // ← 1 回のペイントで反映
}

const t0 = performance.now();
renderData(data);
ws.setFrozenRows(1);
console.log(`Initial load: ${(performance.now() - t0).toFixed(0)}ms`);

ここで重要なのは 2 点です。

1. suspendRender() / resumeRender()

バルク書き込みをこれで囲わないと、setValue のたびに再描画がスケジュールされます。たとえ requestAnimationFrame のレートでも、ループ中に毎秒 60 回ペイントが無駄に走ることになります。suspend/resume で囲めば、レンダラーはループ完了後にちょうど 1 回だけペイントします。

2. DOM ミューテーションではなく、スタイルペイロード

setStyle({ color: '#16a34a' }) はセルのスタイルレコードを更新するだけです。className を切り替えたり、ノードを追加したり、DOM に触れたりはしません。次のペイントでレコードから新しい色が読まれ、描画されます。

このパターンでメモリ的に厳しいデータセットには、専用のバルクロード API である bulkSetCells() を使えます。これは onBulkCellsChange をセルごとではなく一度だけ発火します。


フレーム単位のコスト: v1.2 が Canvas 呼び出しの 70% を削減した場所

10,000 セルをオーバーヘッドなく描画することは必要条件ですが、十分ではありません。より難しい問題は スクロール中 に起きます。レンダラーは毎フレーム新しい可視ウィンドウを走査し、セルごとの Canvas API 呼び出しには測定可能なコストがあります。ctx.font = '…' はフォントシェイピングをトリガーします。ctx.measureText() はテキスト計測パスを走らせます。密なシートではこれらが急速に積み上がります。

v1.2 はこれらにフレーム単位のキャッシュを導入しました。1200×100 セル(テキスト中心、混在スタイル)のシートでフレーム単位に計測した結果がこちらです。

Canvas APIv1.1v1.2削減率
ctx.font=12934−74%
ctx.fillStyle=17946−74%
ctx.measureText()12635−72%
save() / restore()12285−30%

キャッシュキーは文字列そのものです — レンダラーは、設定する値が変わらない場合に代入そのものをスキップします。さらに、折り返し/複数行テキストレイアウト用のフレーム単位キャッシュも追加され、これまで折り返しセルごとに毎フレーム走っていたトークン単位の measureText ループを排除しました。1 行テキストはレイアウトキャッシュを完全にスキップする高速パスを通ります。

ユーザー視点での効果としては、密なシートで以前は 45〜50fps まで落ちていたスクロールフレームが 60fps に張り付き、バックグラウンドタブの CPU 使用率も目に見えて下がります。


10,000 行のソートと再描画

デモでは並び替えは JS 側で行い、結果を同じ renderData() パスで再描画しています。

const t0 = performance.now();
const sorted = [...data].sort((a, b) => b.revenue - a.revenue);
renderData(sorted);
const elapsed = performance.now() - t0;
// → ~60〜120ms 合計(ソート + フル再描画を含む)

すべてのセルを再ペイントしてもこれが速い理由は 2 つあります。

  1. ソートはプレーンなメモリ上で完結。 グリッドとのラウンドトリップもイベントの行き来もありません。
  2. renderData()suspendRender() / resumeRender() で囲まれている ため、バルク書き込みパスが 1 回で済みます。

データが JS 配列ではなくグリッド自身に格納されているシートには、RangeHandle 上の組み込みソート API があり、Canvas バックエンドのセルストアに直接作用します。


ウィンドウ枠固定はコストフリー

ws.setFrozenRows(1);

スクロール中もヘッダー行は固定されたままです。パフォーマンスコストはありません。レンダラーはすでに固定ペインとスクロールペインを別々のビューポート(ViewportController)として管理しており、固定操作は単にどちらのビューポートに行が属するかを切り替えるだけです。同じことが固定列、および 4 ペインの固定分割にも当てはまります。


本ライブラリが目指していないもの

正直に書きます。ReoGrid Web はスプレッドシートであり、データベースのテーブルビューアではありません。

  • 100 万行はスコープ外。 ユースケースは、編集可能で Excel 互換のドキュメント — インボイス、予算表、ダッシュボード、設定シート、社内ツール — です。10 億行のログをスクロールしたいなら、別の抽象(たとえば仮想化テーブル + スティッキースクロール)が必要になります。
  • セルが保持するのは値とスタイルであって DOM ノードではありません。 セル内に本格的な React コンポーネント(日付ピッカー、カスタムフォームなど)を埋め込みたい場合、ReoGrid のセル種別レジストリでチェックボックス・ドロップダウン・ボタン・プログレスバー・レーティング・スパークライン・リンクといったインタラクティブウィジェットは扱えますが、セルごとに DOM ツリーを持つグリッドの代替にはなりません。

それでも、想定するユースケース — ブラウザでの Excel 忠実度の高い編集をインタラクティブな速度で — においては、DOM 仮想化グリッドが自身の描画パイプラインと格闘し始めるようなワークロードでも 60fps を維持できます。


試してみる

ビッグデータデモ は 10,000 行をライブで読み込み、並び替えボタンには再描画時間を測るストップウォッチが組み込まれています。クリック、スクロール、DevTools のパフォーマンスパネルが残りを語ってくれます。

ご自身のプロジェクトで使う場合:

npm install @reogrid/lite     # 無料、100 行 × 26 列まで
# または
npm install @reogrid/pro      # 全機能、10k+ 行対応

シリーズの次回は、実際の .xlsx ファイルをサーバーに送らずブラウザでインポートする方法 — ドラッグ&ドロップ、アップロードなし、Excel フィデリティを維持 — を解説します。

ReoGrid Web を試してみる

React/Vue 向けの Canvas ベース Excel 互換スプレッドシートコンポーネント。 Lite は無料 — npm install 一発で始められます。

関連記事

ニュースレター

開発の最新情報をお届けします

新しいリリース・機能追加・お知らせをいち早く受け取るには、
メーリングリストにご登録ください。