"数百万行でも快適なエディタを作る:ブラウザの限界を超える仮想レンダリングの実装"
テキストエディタを自作する際、避けて通れないのが「極端に巨大なファイルへの対応」です。
10万行、100万行といったテキストをそのままDOMとして描画すれば、ブラウザは一瞬でフリーズします。
これを解決するのが「仮想レンダリング(Virtual Rendering)」ですが、実はブラウザ(特に Chromium 系)には 「要素の高さ制限(約3,355万ピクセル)」 という物理的な壁が存在します。
現在開発中のエディタ elecxzy で、この制限をどのように回避し、数千万行でも軽快なスクロールを実現しているか、その実装を解説します。

1. 仮想レンダリングの基本構造
仮想レンダリングは、「画面に見えている範囲(+上下の予備)だけを描画する」 ものです。
elecxzyでは、以下の 3 つのステップで描画を制御しています。
- 物理スクロール位置から「仮想的なスクロール位置(論理座標)」を算出する。
- 論理座標と 1 行の高さから、描画すべき行の範囲(
startLine〜endLine)を決める。 - 描画対象の行を、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. パフォーマンスを支える工夫
巨大なファイルを扱う際、描画以外にもボトルネックが存在します。
-
データの部分取得:
elecxzyが採用しているPieceTable構造により、数千万行の中から特定の範囲(例:300行目〜350行目)だけを で高速に取得しています。O(\log N) -
計測の高速化:
カーソル位置の計算などで文字列の幅を測る際、DOM を介さず Canvas API で計測することで、レイアウト・スラッシング(レイアウト再計算によるパフォーマンス低下)を防いでいます。 -
予備バッファ:
画面に見える範囲の上下にVIRTUAL_SCROLL_BUFFER_LINES(現在は 30 行)の余白を持たせて描画することで、高速スクロール時のチラつきを抑えています。
5. まとめ
ブラウザという制約の多い環境で、ネイティブエディタに匹敵する体験を作るには、「OS やブラウザが見ている物理的な座標」と「エディタが管理する論理的な座標」を明確に分離 することが不可欠です。
- スケーリング層 でブラウザの高さ制限を突破する。
- 1:1 の座標変換 で描画のガタつきを排除する。
- データ構造(PieceTable) でアクセス速度を保証する。
これらの組み合わせにより、Webベースであっても「数千万行をストレスなく操る」ことが可能になります。
Discussion