不可能なエンジニアリング:ブラウザで大規模なコード差分をレンダリングする方法

プルリクエストを開いたとき、シームレスな体験を期待するものです。小規模な変更であれば、ワークフローは直感的です。しかし、エージェントが生成したコード、大規模なリファクタリング、あるいは広範なテストスナップショットなど、規模が大きくなるにつれて、レビュー画面はしばしば劣化します。ナビゲーションは鈍くなり、ファイルは一つずつ読み込まれ、ブラウザは苦戦し始めます。

大規模な差分をレンダリングすることは、一見単純に見える問題です。コードは「単なるテキスト」ですが、プロフェッショナルなレビュー画面には、構文ハイライト、行番号、注釈、そして柔軟なレイアウトが必要です。これらの機能が数千のファイルや数百万行にわたって増殖すると、ブラウザのDOM、メモリ、および処理の限界にすぐに達してしまいます。これを解決するために、Diffs のチームは、*「どんな差分でもただレンダリングできなければならない」*という、単一かつ野心的な目標に基づいて設計された、仮想化優先のコンポーネント CodeView を開発しました。

三つの脅威:レンダリング、処理、およびメモリ

スケーラブルなレビュー画面を構築するために、開発者は3つの主要なボトルネックを特定しました:

  1. レンダリング: DOMの複雑さはコードの量に比例して増加し、スクロールやインタラクション中にブラウザに過負荷をかけます。
  2. 処理: 構文ハイライトのような操作は、単一のファイルでは安価ですが、数千回繰り返されると非常に高価になります。
  3. メモリ: 大規模なパッチファイルをレンダリング用のデータ構造に変換すると、ブラウザのメモリ限界を押し上げ、頻繁で中断を伴うガベージコレクションを誘発する可能性があります。

「ブランキング」問題の「逆スタッキー・バーチャライゼーション」による解決

仮想化(またはウィンドウイング)は、ビューポート付近のコンテンツのみをレンダリングすることでDOMサイズを削減する標準的なアプローチです。しかし、従来の仮想化では、ユーザーがJavaScriptによる次のコンテンツのレンダリングよりも速くスクロールした場合に、空白(ホワイトスペース)が残る「ブランキング」現象がよく発生します。

バッファ付きのネイティブスクロールや requestAnimationFrame を使用したスタッキーコンテナなど、いくつかの手法を評価した後、チームは Inverse Sticky Technique(逆スタッキー手法) を開発しました。

通常のスタッキー(粘着)ポジショニングでは、要素はビューポートの上部に留まります。Inverse Sticky Technique では、レンダリング領域の底部がスクロールダウン時にはビューポートの底部に、スクロールアップ時には上部に留まります。これは、負の top および bottom sticky オフセットを (contentHeight - viewportHeight) * -1 として計算することで実現されます。

このハイブリッドアプローチは、ネイティブなブラウザのスクロールと慣性を維持しつつ、ブランキングを事実上不可能にします。JavaScriptの実行がスクロール位置に遅れる場合でも、レンダリング領域が単にビューポートの端に「張り付く」からですです。

スケーラブルなレイアウトとスクロールアンカリング

仮想化をスムーズに感じさせるためには、エンジンはスクロールバーの「跳ね」を避けるために、コンテンツの総高さを正確に推定する必要があります。CodeView は、安価な第一段階の推定値を使用します:(lineHeight * totalLines) + (hunkSeparatorHeight * hunkCount)

病的なほど巨大なファイル(例:数十万行のハンク)を扱うために、チームはキャッシュされた position-to-line チェックポイント・システム を実装しました。これにより、エンジンはファイルの先頭から反復処理を行うのではなく、バイナリサーチを使用して開始レンダリング範囲を見つけることができます。

さらに、仮想化されたDOMは常に変化するため、ブラウザのネイティブなスクロールアンカリングは機能しません。CodeView は独自のアンカリング・ロジックを実装しています:

  • 最初に完全に表示されている行またはファイルを特定します。
  • これをビューポートのオフセットを持つアンカーとして保存します。
  • DOMの変更を確定し、高さの差分を調整した後、スクロール位置を調整して、アンカーが同じオフセットに留まるようにし、表示の跳ねを跳ね返します。

異常なケースに対するメモリ最適化

Linux v6 と v7 の差分のような、大規模なデータセットに対するテストでは、メモリ管理がレンダリング速度と同じくらい重要であることが明らかになりました。

解析済み文字列の切り離し

JavaScriptにおいて、サブストリングは元の大きな文字列への参照を保持し続けることがあります。700 MB のパッチファイルを解析する場合、必要な行だけを保持しても、誤ってソース文字列全体をメモリに保持し続けてしまう可能性があります。文字列を明示的にコピーして元の入力から切り離すことで、チームは Linux の差分におけるメモリ使用量を 2.4 GB から 1.15 GB に削減し、解析時間を 80% 改善しました。

DOMプーリングと共有状態

スクロール中の絶え間ないDOMの変動によるガベージコレクションの停止(ポーズ)を減らすため、CodeViewDOMプーリング を採用しています。各ファイルごとに Shadow DOM ラッパー(スタイルシートやSVGアイコンを含む)を破棄して再作成するのではなく、エンジンはこれらのシェルを再利用し、内部コンテンツのみを入れ替えます。 n、さらに、チームは設定管理を最適化しました。元々は、すべてのファイルが独自の options オブジェクトを持っていました。数十万のファイルに対して、単一の設定(行の折り返しを切り替えるなど)を変更する場合、すべてのインスタンスを反復処理して更新する必要がありました。状態を CodeView の共有の真実のソース(source of truth)に移動し、アイテムに特化化したゲッターを使用することで、見た目の変更はレビュー全体を通して設定を書き換えることなく、即座に発生するようになります。

段階的な機能向上(Progressive Enhancement)による遅延ハイライト

構文ハイライトは、最も計算コストの高いタクションです。メインスレッドをブロックしないように、CodeView は遅延パイプラインを使用します:

  1. 即時レンダリング: コードはまずプレーンテキストとして即座にレンダリングされます。
  2. Worker Offloading: ハイライトのリクエストは、Shiki を実行する Web Worker プールの集まりに送られます。
  3. LRU Caching: 結果は Least Recently Used キャッシュに保存され、スクロールして再び表示されるコードの再処理を回避します。

残された課題と今後の方向性

これらの成果にもかかわらず、チームはブラウザ環境には依然として限界があることを認めています。アグレッシブなスクロール中に CSS レイアウトとペイントは依然として高価であり、また、大規模なファイルのためのハイライト済みデータのシリアライズはメインスレッドを支配することがあります。

仮想化に関する哲学的な議論もあります。Hacker News のディスカッションで一部のコミュニティメンバーが指摘したように、一部の人は、現代のハードウェアは「狂ったようなトリック」を使わずに大規模な差分をハンドルできるはずだと主張し、他の人は、差分の将来は行ベースのレンダリングではなく、AST (Abstract Syntax Tree) 差分であると示唆しています。

それに関わらず、現在の CodeView の実装は、ブラウザで可能なことの境界を押し広げており、レビューアーのフォーカスがツールではなくコードに集中できるように、PRが 10 行であっても 1000 万行であっても、確実に実現しています。

Sources