ブログへ戻る
火曜日、2026年1月20日
投稿者: Anthony Shew (@anthonysheww)、Benjamin Woodruff (@_bgwoodruff)、Tobias Koppers (@wSokra)
編集。保存。更新。待つ…待つ…待つ…
コンパイルは通常「待ち」を伴いますが、Turbopackはキャッシュと増分計算により反復ループを高速化します。すべてのモダンなバンドラが増分アプローチを採用しているわけではなく、それには十分な理由があります。増分計算は大きな複雑さとバグの温床になり得ます。キャッシュは追加の追跡やデータのコピーを必要とし、CPUとメモリのオーバーヘッドを増やします。誤った適用はパフォーマンスを悪化させることさえあります。それでも我々がこの挑戦を受けたのは、増分アーキテクチャがTurbopackの成功に不可欠だと確信していたからです。
TurbopackはNext.jsの新しいデフォルトバンドラです。Next.jsは世界で最も大規模なウェブアプリケーションのいくつかを構築するために使われているフレームワークであり、最大規模かつ最も困難なワークロードでもインスタントなビルドと入力に合わせた高速なReact Fast Refresh体験を実現する必要がありました。増分アーキテクチャはこれを達成するための中核です。
キャッシュなしのソリューションはアプリケーションの規模が増すと遅くなります。増分計算は、小さな反復変更のサイズにのみ依存するため、大規模アプリケーションに対してスケールできます。Turbopackのアーキテクチャは最初からキャッシュを念頭に置いて設計されており、増分設計は10年以上の研究に基づいています。webpackでのキャッシュ実装の経験から得た教訓を踏まえ、Salsa(Rust-AnalyzerやRuffを支える)、Parcel、Rustコンパイラのクエリシステム、Adapton、その他多くから着想を得ています。
Turbopackは、内部関数がどのように呼び出され、どの値に依存しているかを自動的に追跡することで、細粒度のキャッシュを実現します。何かが変更されたとき、最小限の作業で結果を再計算する方法がわかります。
背景:手動の増分計算
多くのビルドシステムは、ビルドルールを評価するときに手動で構築する明示的な依存グラフを含みます。依存グラフを明示的に宣言すれば理論上は最適にできますが、実際にはエラーの余地が残ります。明示的依存グラフの指定が難しいため、実務では通常キャッシュは粗いファイル単位の粒度で行われます。この粒度には利点もあります:増分結果が少なければキャッシュするデータも少なく、ディスクやメモリが限られている場合には有利です。GNU Makeのように、出力ターゲットと前提条件を手動でファイルとして設定するアーキテクチャは、このような代表例です。こうしたシステムは、コンパイラ内部のデータ構造を理解できないためキャッシュの機会を逃します。
関数レベルの細粒度自動増分計算
Turbopackでは、入力ファイルと生成されるビルド成果物の関係は単純ではありません。バンドラはデッドコード削除("tree shaking")のための全プログラム解析やモジュールグラフにおける共通依存のクラスタリングを行います。その結果、ビルド成果物(複数のアプリケーションルートで共有されるJavaScriptファイル)は入力ファイルと多対多の複雑な関係を形成します。
手動で依存関係を宣言してグラフに追加することは人為的ミスを生みやすいため、Turbopackはスケールする自動化された解を必要としました。
値セル(value cells)でのコンパイルグラフ追跡
自動キャッシュと依存関係追跡を可能にするために、Turbopackは「値セル」(Vc<...>)という概念を導入しています。各値セルはスプレッドシートのセルのような、実行の細かい部分を表します。セルを読み込むとき、その時点で実行中の関数とその関数が参照するすべてのセルが、そのセルに依存していると記録されます。これはSolidJSのようなフレームワークでのシグナルに似ています。値セルはawaitされたときに追跡されます。セルを読み取るまで依存関係としてマークしないことで、従来のトップダウンなメモ化アプローチよりも細粒度のキャッシュを実現しています。
例えば、引数が多くの値セルを含むオブジェクトやマッピングである場合、そのオブジェクトやマッピングの一部が変わったからといって追跡された関数を再計算する必要はありません。実際にその関数が読んだセルが変更されたときだけ再計算すればよいのです。
値セルはTurbopack内のほぼすべてを表します。例えばディスク上のファイル、抽象構文木(AST)、モジュールのimport/exportに関するメタデータ、チャンク化やバンドルに用いるクラスタリング情報などです。
- 値セルに格納され得るデータの例:ファイル、AST、インポート/エクスポートのメタデータ、チャンク化情報
ダーティマークと変更の伝播
Turbopackが関数を初回実行するとき、関数とそれらが作成または依存する値セルのグラフを構築します。グラフの根は要求された出力(バンドル済みアセット)で、葉はソースコードファイルです。中間にはAST、メタデータ、部分的に変換されたモジュール、チャンク情報といった中間表現があります。
ファイルシステムウォッチャーがソースの変更を検出すると、そのファイルの値セルを読んでいるすべての関数を「ダーティ(dirty)」としてマークし、再計算キューに入れます。ダーティになった関数がそのファイルを読み、JavaScriptモジュールをパースまたは変換して新しい中間表現を生成するかもしれません。関数を再計算すると、変更された中間表現を含むセルが更新され、さらに多くの関数がダーティになる可能性があります。セルの更新はセル内容が等しい場合はスキップされます。この伝播は影響を受けた関数がすべて再計算されるまでグラフの上位へ向かって波及します。
追加の最適化として、実行は「需要駆動(demand-driven)」です。つまり、ダーティな関数の再実行はそれらが「アクティブクエリ」の一部になるまで遅延されます。開発時のアクティブクエリはホットリロードが有効な現在開いているウェブページかもしれません。ビルド時のアクティブクエリはフルの本番アプリの要求です。
(記事内では、初期(コールド)実行、ファイル変更時の「マークダーティ」操作、葉から根への伝播を示す例示的な呼び出しツリーが示されています。)
集約グラフ(Aggregation graphs)
ほとんどの操作、例えばダーティ伝播アルゴリズムのようなものは隣接エッジや隣接ノードの情報だけを必要としますが、一部の操作は依存グラフのより大きな部分に対する問い合わせを必要とします。
- サブグラフが新しいアクティブクエリの一部になったときにすべてのダーティノードを見つけて再計算をスケジュールする。
- サブグラフのエラー、警告、またはlint結果を収集する。
- サブグラフの計算完了を待つ。
細粒度のキャッシュを維持していると、グラフは数十万〜数百万の中間結果を含むことがあります。このグラフの大きな部分を都度走査するのは高コストです。これらのクエリを効率化するために、Turbopackは依存グラフの上に追加のデータ構造として「集約グラフ」を使います。依存グラフを構築・更新するときに、依存グラフの一部を要約する並列ノードを集約グラフに維持します。エラーや警告のような頻繁にアクセスされる情報は集約ノードに付随します。
集約グラフは複数の解像度レイヤーを持ち、上位の集約レイヤーは各ノードがより多くの関数を参照することで解像度を下げ、情報収集時に走査すべきノード数を減らします。あらゆる潜在的アクティブクエリ(例:アプリケーションのエントリポイントやルート)は集約グラフにおけるルートを表します。最終的な集約グラフレイヤーでは、各ルートは元の依存グラフにおける自分自身とすべての子を表しています。依存グラフにルートを追加するには集約グラフの再編成が必要になることがありますが、それは稀な操作です。
集約レイヤーは元の依存グラフより詳細を減らしますが、走査は高速になります。最終レイヤーの集約ルートノードは元のグラフのすべての子を含みます。
ファイルシステムキャッシュ
Next.js 16.1リリースまでは、これらのキャッシュはすべてメモリ上のみで保持されていました。この新しいリリースでは、next dev向けのファイルシステムキャッシュを安定化させ、デフォルトでオンにして出荷しました。このキャッシュにより、依存グラフ、集約グラフ、そして値セルに格納されたすべての中間結果をディスクに永続化できます。next devを再起動すると、このウォームキャッシュから素早く再開できます。
ファイルシステムキャッシュには独自の課題があり、我々の高いパフォーマンスと品質基準を満たすために1年以上の専従作業を要しました。これについては今後のエンジニアリングブログで詳述します。
フィードバックとコミュニティ
フィードバックを共有し、Next.jsの未来形成に参加してください:
- GitHub Discussions
- GitHub Issues
- Discord Community