🚀

"数百万行でも快適なエディタを作る:ブラウザの限界を超える仮想レンダリングの実装"

に公開

テキストエディタを自作する際、避けて通れないのが「極端に巨大なファイルへの対応」です。
10万行、100万行といったテキストをそのままDOMとして描画すれば、ブラウザは一瞬でフリーズします。

これを解決するのが「仮想レンダリング(Virtual Rendering)」ですが、実はブラウザ(特に Chromium 系)には 「要素の高さ制限(約3,355万ピクセル)」 という物理的な壁が存在します。

現在開発中のエディタ elecxzy で、この制限をどのように回避し、数千万行でも軽快なスクロールを実現しているか、その実装を解説します。


1. 仮想レンダリングの基本構造

仮想レンダリングは、「画面に見えている範囲(+上下の予備)だけを描画する」 ものです。

elecxzyでは、以下の 3 つのステップで描画を制御しています。

  1. 物理スクロール位置から「仮想的なスクロール位置(論理座標)」を算出する。
  2. 論理座標と 1 行の高さから、描画すべき行の範囲(startLineendLine)を決める。
  3. 描画対象の行を、1:1 のピクセル精度で物理座標に配置 する。

処理フロー

2. ブラウザの「高さ制限」とスケーリング

通常の仮想スクロールでは、全行の合計高さをコンテナの height に指定します。しかし、1 行 21px の場合、約 160 万行でブラウザの高さ制限(3,355 万ピクセル)を超えてしまい、スクロールが止まったり描画が乱れたりします。

これを回避するため、elecxzyでは論理座標と物理座標の変換比率となる スクロール・ファクター(scrollFactor を導入しています。

スケーリングロジックの実装例

合計高さが安全圏(3,000 万ピクセル)を超える場合、物理的なスクロール範囲を制限値に固定し、論理的な位置との比率を計算します。

// 安全な最大高さ
export const MAX_BROWSER_HEIGHT = 30000000;

const { isScaling, scrollFactor, displayTotalHeight } = useMemo(() => {
    const h = totalLines * lineHeight + PADDING;
    const scaling = h > MAX_BROWSER_HEIGHT;

    // 物理的なコンテンツ高さ
    const pContentHeight = scaling ? MAX_BROWSER_HEIGHT : h;
    
    // スクロール可能な範囲の比率(Factor)を算出
    const vRange = Math.max(1, h - viewportHeight);
    const pRange = Math.max(1, pContentHeight - viewportHeight);
    const factor = scaling ? pRange / vRange : 1;

    return { 
        isScaling: scaling, 
        scrollFactor: factor, 
        displayTotalHeight: pContentHeight 
    };
}, [totalLines, lineHeight, viewportHeight]);

この scrollFactor を介することで、OS標準のスクロール操作を維持したまま、論理的には数億ピクセルの空間を移動可能になります。

3. 描画位置の精密な同期

スケーリングを行っている場合、単純な配置ではスクロール時に微妙なガタつき(ジッター)が発生します。
これを防ぐために、描画コンテナの top 位置を計算で調整し、ビューポートに対して 1:1 のピクセル精度を維持します。

// 物理的なスクロール位置と仮想・論理位置を同期させる計算
const virtualLineStartTop = startLine * lineHeight;
const lineContainerTop = physicalScrollTop + (virtualLineStartTop - scrollTop);

return (
    <div style={{ position: 'relative', height: displayTotalHeight }}>
        <div style={{ position: 'absolute', top: lineContainerTop }}>
            {linesToRender.map((line) => (
                <LineContent key={line.index} {...line} />
            ))}
        </div>
    </div>
);

この lineContainerTop の計算により、物理スクロールと仮想座標の端数(ピクセル未満のズレ)が相殺され、滑らかな描画が実現します。

4. パフォーマンスを支える工夫

巨大なファイルを扱う際、描画以外にもボトルネックが存在します。

  1. データの部分取得:
    elecxzyが採用しているPieceTable構造により、数千万行の中から特定の範囲(例:300行目〜350行目)だけを O(\log N) で高速に取得しています。
  2. 計測の高速化:
    カーソル位置の計算などで文字列の幅を測る際、DOM を介さず Canvas API で計測することで、レイアウト・スラッシング(レイアウト再計算によるパフォーマンス低下)を防いでいます。
  3. 予備バッファ:
    画面に見える範囲の上下に VIRTUAL_SCROLL_BUFFER_LINES(現在は 30 行)の余白を持たせて描画することで、高速スクロール時のチラつきを抑えています。

5. まとめ

ブラウザという制約の多い環境で、ネイティブエディタに匹敵する体験を作るには、「OS やブラウザが見ている物理的な座標」と「エディタが管理する論理的な座標」を明確に分離 することが不可欠です。

  1. スケーリング層 でブラウザの高さ制限を突破する。
  2. 1:1 の座標変換 で描画のガタつきを排除する。
  3. データ構造(PieceTable でアクセス速度を保証する。

これらの組み合わせにより、Webベースであっても「数千万行をストレスなく操る」ことが可能になります。


Discussion