Transcript

00:00:00(アップビートな音楽) 皆さん、こんにちは。ありがとうございます。
00:00:07ルーク・サンドバーグと申します。
00:00:09VercelでソフトウェアエンジニアとしてTurbo Packを担当しています。
00:00:12Vercelに入社して約半年ですが、
00:00:15その間に、
00:00:15私が関わっていない素晴らしい仕事について、
00:00:19このステージで皆さんにお話しする機会を得ました。
00:00:23Vercelの前はGoogleにいて、
00:00:26社内ウェブツールチェーンの開発や、
00:00:28TSXからJavaバイトコードへのコンパイラ構築、
00:00:32Closure Compilerの作業など、
00:00:35変わった仕事をしていました。
00:00:37Vercelに着任した時、まるで別の惑星に来たかのように、何もかもが違っていて驚きました。
00:00:45チームの取り組みや目標には、かなり驚かされましたね。
00:00:50今日は、
00:00:51Turbo Packで採用した設計上の選択肢をいくつかご紹介し、
00:00:54それらが既存の素晴らしいパフォーマンスをさらに向上させる上で、
00:00:58どのように役立つと考えているかをお話しします。
00:01:01その説明として、まず私たちの全体的な設計目標をご覧ください。
00:01:06これを見て、私たちがいくつかの難しい選択をしたことがすぐにわかるでしょう。
00:01:14例えば、コールドビルドはどうでしょう?
00:01:17コールドビルドは重要ですが、私たちの考えでは、そもそもそれを経験すべきではないのです。
00:01:22そして、この話ではそこに焦点を当てていきます。
00:01:24基調講演では、バンドルパフォーマンス向上のために、いかに増分性を活用しているかについて少し触れました。
00:01:31増分性における重要なアイデアは、キャッシングです。
00:01:35バンドラーが行うすべての処理をキャッシュ可能にすることで、
00:01:38変更を加えるたびに、
00:01:39その変更に関連する作業だけをやり直せば済むようにしたいのです。
00:01:43言い換えれば、
00:01:44ビルドのコストは、
00:01:45アプリケーション全体の規模や複雑さではなく、
00:01:49変更の規模や複雑さに応じて決まるべきだということです。
00:01:53こうすることで、
00:01:54どれだけ多くのアイコンライブラリをインポートしても、
00:01:57Turbo Packが開発者に優れたパフォーマンスを提供し続けることを保証できるのです。
00:02:01そのアイデアを理解し、説明するために、世界で最もシンプルなバンドラーを想像してみましょう。おそらくこんな見た目です。
00:02:09これが私たちの「ベビーバンドラー」です。
00:02:12スライドに載せるには少しコードが多すぎるかもしれませんが、これからもっとひどくなりますよ。
00:02:17ここでは、すべてのエントリーポイントをパースします。
00:02:20インポートをたどり、参照を解決し、アプリケーション全体を再帰的に調べて、依存するすべてのものを見つけ出します。
00:02:28そして最後に、各エントリーポイントが依存するすべてを集めて、出力ファイルに書き出すだけです。
00:02:35やった、ベビーバンドラーの完成です。
00:02:38これは明らかに素朴な方法ですが、増分性の観点から見ると、どの部分も増分ではありません。
00:02:45特定のファイルは、インポート回数によっては複数回パースすることになります。これはひどいですね。
00:02:53Reactのインポートは、間違いなく何百回、何千回も解決することになるでしょう。
00:02:57ええ、痛いですよね。
00:03:01そこで、これをもう少しでも増分的にしたいなら、冗長な作業を避ける方法を見つける必要があります。
00:03:08では、キャッシュを追加してみましょう。
00:03:10これが私たちのパース関数だと想像してください。
00:03:15とてもシンプルですね。
00:03:15そして、おそらくバンドラーの主力となる部分でしょう。
00:03:19ええ、非常にシンプルです。
00:03:19ファイルの内容を読み込み、SWCに渡してASTを取得します。
00:03:25では、キャッシュを追加しましょう。
00:03:27なるほど、これは明らかにシンプルで良い改善点です。
00:03:31しかし、皆さんの何人かは、以前にキャッシングコードを書いた経験があるでしょう。
00:03:36ここにはいくつかの問題があるかもしれません。
00:03:38例えば、ファイルが変更されたらどうなるでしょう?
00:03:41これは明らかに私たちが考慮すべき点です。
00:03:46それに、もしファイルが実際にはファイルではなく、トレンチコートを着た3つのシンボリックリンクだったらどうでしょう?
00:03:52多くのパッケージマネージャーは、そのように依存関係を整理します。
00:03:55そして、ファイル名をキャッシュキーとして使っています。
00:03:59それで十分でしょうか?
00:04:00例えば、クライアントとサーバーの両方のためにバンドルしています。
00:04:03同じファイルが両方に含まれることになります。
00:04:04それでうまくいくでしょうか?
00:04:05また、ASTを保存して返しています。
00:04:08だから、今度はミューテーションについて心配する必要があります。
00:04:11そして最後に、これは本当に素朴なパース方法ではないでしょうか?
00:04:16皆さんがコンパイラに膨大な設定をしていることは知っています。
00:04:21その一部はここに含まれるはずです。
00:04:23ええ、これらはすべて素晴らしいフィードバックですね。
00:04:27そして、これは非常に素朴なアプローチです。
00:04:32そしてそれに対して、もちろん私は「ええ、これはうまくいきません」と答えるでしょう。
00:04:36では、これらの問題を解決するためにどうすればいいでしょう?
00:04:39修正して、間違いがないようにしてください。
00:04:44では、よし。
00:04:46これなら少しは良くなるかもしれません。
00:04:49ご覧の通り、いくつかの変換がありますね。
00:04:52各ファイルにカスタマイズされた処理を行う必要があります。例えば、ダウンレベリングや`use cache`の実装などです。
00:04:58また、いくつかの設定もあります。
00:05:00ですから、もちろんそれをキャッシュのキーに含める必要があります。
00:05:04しかし、もしかしたらすぐに疑念を抱くかもしれません。
00:05:08例えば、「これで正しいのか?」と。
00:05:09例えば、名前だけで変換を識別するのに十分なのでしょうか?
00:05:13わかりません。もしかしたらそれ自体が複雑な設定を持っているかもしれません。
00:05:16そして、この2つのJSON値は、私たちが考慮すべきすべてを実際に捉えることができるのでしょうか?
00:05:24開発者はこれを維持してくれるでしょうか?
00:05:26これらのキャッシュキーはどれくらいの大きさになるでしょう?
00:05:29設定のコピーはいくつ持つことになるでしょう?
00:05:31私は実際にこのようなコードを見たことがありますが、そのロジックを理解するのはほとんど不可能だと感じています。
00:05:37さて、無効化に関する別の問題も解決しようとしました。
00:05:43そこで、ファイル読み込みにコールバックAPIを追加しました。
00:05:46これは素晴らしいですね。ファイルが変更されたら、
00:05:49キャッシュから削除するだけで、
00:05:51古いコンテンツを提供し続けることはありません。
00:05:55しかし、
00:05:55これはかなり素朴です。確かにキャッシュを削除する必要はありますが、
00:05:59呼び出し元も新しいコピーを取得する必要があることを知る必要があるからです。
00:06:03では、コールバックをスレッド化していきましょう。
00:06:06よし、できました。
00:06:09スタックを通じてコールバックをスレッド化しました。
00:06:12ここで、呼び出し元が変更を購読できるようにしていることがわかります。
00:06:16何か変更があればバンドル全体を再実行できますし、ファイルが変更されればそれを呼び出します。
00:06:22素晴らしい、リアクティブなバンドラーの完成です。
00:06:25しかし、これでもまだほとんど増分ではありません。
00:06:28だから、ファイルが変更されたら、すべてのモジュールを再度ウォークして、すべての出力ファイルを生成する必要があります。
00:06:37パースキャッシュで多くの作業を節約できましたが、これだけでは十分ではありません。
00:06:45そして最後に、他にもすべての冗長な作業があります。
00:06:49例えば、インポートは絶対にキャッシュしたいですね。
00:06:52ファイルを何度も見つけることがあり、そのインポートが常に必要になるので、そこにキャッシュを置きたいです。
00:06:57そして、
00:06:58解決結果は実際にはかなり複雑なので、
00:07:00Reactの解決で行った作業を再利用できるように、
00:07:04間違いなくそれをキャッシュすべきです。
00:07:08しかし、さて、今度は別の問題があります。
00:07:11依存関係を更新したり、新しいファイルを追加したりすると、解決結果が変わるので、そこにも別のコールバックが必要です。
00:07:18そして、
00:07:18出力生成ロジックも絶対にキャッシュしたいです。HMRセッションで考えると、
00:07:24アプリケーションの一部を編集しているだけなのに、
00:07:28なぜ毎回すべての出力を書き換えるのでしょうか?
00:07:31また、出力ファイルを削除することもあるでしょうから、そこでの変更もリッスンすべきです。
00:07:39さて、
00:07:39これらすべてを解決したかもしれませんが、
00:07:42まだこの問題があります。それは、
00:07:44何かが変更されるたびに、
00:07:46最初からやり直すということです。
00:07:48だから、
00:07:48この関数の制御フロー全体が機能しません。なぜなら、
00:07:51単一のファイルが変更された場合、
00:07:53そのforループの途中に飛び込みたいからです。
00:07:56そして最後に、呼び出し元への私たちのAPIも絶望的に素朴です。
00:08:03彼らはおそらく、どのファイルが変更されたかを知りたいでしょう。そうすれば、クライアントに更新をプッシュできます。
00:08:07ええ。
00:08:11だから、このアプローチは実際には機能しません。
00:08:13そして、
00:08:13たとえこれらのすべての場所ですべてのコールバックをスレッド化したとしても、
00:08:18このコードを実際に維持できると思いますか?
00:08:21新しい機能を追加できると思いますか?
00:08:24私はそうは思いません。
00:08:25これはただクラッシュして燃え尽きるだけだと思います。
00:08:28そして、それに対して、私は「ええ」と答えるでしょう。
00:08:34では、もう一度、どうすべきでしょうか?
00:08:36LLMとチャットするときと同じように、まず自分が何を求めているのかを知る必要があります。
00:08:43そして、それを非常に明確にする必要があります。
00:08:48では、私たちは何を求めているのでしょうか?
00:08:50私たちは多くの異なるアプローチを検討しましたし、チームの多くのメンバーは実際にバンドラーでの作業経験が豊富でした。
00:08:59そこで、このような大まかな要件をまとめました。
00:09:02バンドラー内のすべての高コストな操作をキャッシュできるようにしたいのは間違いありません。
00:09:05そして、これは本当に簡単に行えるべきです。
00:09:08例えば、新しいキャッシュを追加するたびに、コードレビューで15件のコメントをもらうべきではありません。
00:09:12そして、
00:09:13私は開発者が正しいキャッシュキーを書いたり、
00:09:17入力や依存関係を手動で追跡したりすることを、
00:09:21実際にはあまり信用していません。
00:09:24だから、私たちがそれを処理すべきです。
00:09:26これは間違いなく誰にでも使えるようにすべきです。
00:09:30次に、変更される入力を処理する必要があります。
00:09:33これはHMRにおける大きなアイデアのようなものですが、セッション間でも同様です。
00:09:36だから、ほとんどはファイルになりますが、設定のようなものも含まれます。
00:09:40そして、ファイルシステムキャッシュでは、実際には環境変数のようなものも含まれることになります。
00:09:45だから、私たちはリアクティブでありたいです。
00:09:47何か変更があればすぐに再計算できるようにしたいですし、
00:09:50どこにでもコールバックをスレッド化するようなことはしたくありません。
00:09:54最後に、最新のアーキテクチャを活用し、マルチスレッドで、一般的に高速である必要があります。
00:10:02だから、この要件のセットを見て、「これがバンドラーと何の関係があるんだ?」と考えている方もいるかもしれません。
00:10:12そしてそれに対して、もちろん私の経営陣がこの部屋にいるので、それについて話す必要はあまりありません。
00:10:20しかし実際には、皆さんの多くはもっと明白な結論に飛びついたのではないでしょうか。
00:10:24これはシグナルにとても似ていますね。
00:10:28ええ、私はシグナルのようなシステムを説明しています。
00:10:31これは、計算を構成し、依存関係を追跡し、ある程度の自動メモ化を行う方法です。
00:10:37そして、
00:10:38私たちはあらゆる種類のシステム、
00:10:40特にRustコンパイラとSalsaと呼ばれるシステムからインスピレーションを得たことに注意すべきです。
00:10:45興味があれば、これらの概念に関するAdaptonsという学術文献もあります。
00:10:51さて、
00:10:51では実際にこれがどのように見えるかを見てみましょう。そして、
00:10:55JavaScriptのコードサンプルからRustへと、
00:10:59非常に不自然なジャンプをします。
00:11:01これが私たちが構築したインフラストラクチャの例です。
00:11:05TurboTask関数は、私たちのコンパイラにおけるキャッシュされた作業単位です。
00:11:12このように関数にアノテーションを付けると、
00:11:16それを追跡し、
00:11:17そのパラメータからキャッシュキーを構築できます。これにより、
00:11:22必要に応じてキャッシュしたり、
00:11:25再実行したりできます。
00:11:28ここにあるVC型は、
00:11:29シグナルのように考えることができます。これはリアクティブな値で、
00:11:33VCはvalue cellの略ですが、
00:11:36signalの方が少し良い名前かもしれません。
00:11:39このようにパラメータを宣言すると、「これは変更される可能性があり、変更されたら再実行したい」と言っていることになります。
00:11:47では、どうやってそれを知るのでしょうか?
00:11:49そこで、これらの値を`await`を介して読み込みます。
00:11:52このようにリアクティブな値を`await`すると、自動的に依存関係を追跡します。
00:11:58そして最後に、もちろん、私たちがやりたかった実際の計算を行い、それをセルに保存します。
00:12:07依存関係を自動的に追跡しているので、この関数がファイルの内容と設定の値の両方に依存していることがわかります。
00:12:17そして、
00:12:18新しい結果をセルに保存するたびに、
00:12:21以前の結果と比較し、
00:12:23変更があれば、
00:12:24その値を読み込んだすべての人に通知を伝播できます。
00:12:29この「変更」という概念が、私たちの増分性へのアプローチの鍵となります。
00:12:33ええ、繰り返しになりますが、最もシンプルなケースはここにあります。
00:12:37ファイルが変更されると、Turbo Packはそれを検知し、この関数実行を無効化し、すぐに再実行します。
00:12:45そして、たまたま同じASTを生成した場合、同じセルを計算するので、そこで停止します。
00:12:53ファイルをパースする場合、ASTを実際に変更しないような編集はほとんどありません。
00:13:00しかし、Turbo Pack関数の基本的な構成可能性を活用して、これをさらに進めることができます。
00:13:07ここでは、モジュールからインポートを抽出する別のTurbo Packキャッシュ関数を見ています。
00:13:15これはバンドラーで非常によくあるタスクだと想像できるでしょう。
00:13:20アプリケーション内のすべてのモジュールを実際に見つけるためだけに、インポートを抽出する必要があります。
00:13:25それらを利用して、モジュールをチャンクにまとめる最適な方法を選択します。
00:13:29そしてもちろん、インポートグラフはツリーシェイキングのような基本的なタスクにとって重要です。
00:13:34そして、インポートデータの消費者が非常に多いため、キャッシュは非常に理にかなっています。
00:13:41だから、この実装は特に特別なものではありません。
00:13:44これは、どんなバンドラーでも見られるようなものです。
00:13:46ASTをウォークし、インポートを私たちが好む特別なデータ構造に収集し、それを返します。
00:13:55しかし、ここでの重要なアイデアは、それらを別のセルに保存することです。
00:13:58だから、モジュールが変更された場合、それを読み込むので、この関数を再実行する必要があります。
00:14:05しかし、モジュールに加える変更の種類を考えると、実際にインポートに影響を与えるものはごくわずかです。
00:14:12だから、モジュールを変更したり、関数本体、文字列リテラル、あらゆる実装の詳細を更新したりします。
00:14:20それは、この関数を無効化し、その後、同じインポートのセットを計算します。
00:14:25そして、これを読み込んだものは何も無効化しません。
00:14:29だから、
00:14:29HMRセッションでこれを考えると、
00:14:32ファイルを再パースする必要はありますが、
00:14:36チャンキングの決定方法について考える必要はもうありません。
00:14:40それらが変更されていないことを知っているので、ツリーシェイキングの結果について考える必要はありません。
00:14:45だから、ファイルをパースし、このシンプルな分析を行い、すぐにアウトプットの生成にジャンプできます。
00:14:53そして、これが私たちが非常に高速なリフレッシュタイムを実現している方法の一つです。
00:14:57だから、これはかなり命令的です。
00:15:02この基本的なアイデアを別の方法で考えるなら、ノードのグラフとして捉えることができます。
00:15:06左側には、コールドビルドを想像できるでしょう。
00:15:12最初は、実際にすべてのファイルを読み込み、すべてをパースし、すべてのインポートを分析する必要があります。
00:15:17そしてその副作用として、アプリケーションからすべての依存関係情報を収集しました。
00:15:21そして何かが変更されたとき、
00:15:24構築した依存関係グラフを活用して無効化を伝播し、
00:15:28スタックを遡ってTurbo Pack関数を再実行できます。
00:15:32そして、もし新しい値を生成すれば、そこで停止します。
00:15:35そうでなければ、無効化を伝播し続けます。
00:15:37素晴らしい。
00:15:41ご想像の通り、これは私たちが実際に行っていることの、かなり大規模な過剰な単純化です。
00:15:47今日のTurbo Packには、約2,500種類のTurboタスク関数があります。
00:15:53そして、典型的なビルドでは、文字通り何百万もの異なるタスクがあるかもしれません。
00:15:58だから、実際にはもう少しこんな感じに見えるかもしれません。
00:16:01さて、これを皆さんが読めることを期待しているわけではありません。
00:16:04スライドには収まりませんでした。
00:16:06だから、ズームアウトすべきかもしれません。
00:16:08なるほど、それは明らかに役立ちませんね。
00:16:14実際には、Turbo Pack内で何が起こっているかを追跡し、視覚化するためのより良い方法があります。
00:16:21しかし、根本的には、それらは依存関係情報の大部分を捨て去ることで機能します。
00:16:26そして今、皆さんの何人かはシグナルでの作業経験があるかもしれませんね。もしかしたら悪い経験かもしれません。
00:16:34私は個人的にスタックトレースが好きで、デバッガーで関数に出入りできるのが好きです。
00:16:41だから、これが完全な万能薬だと疑っているかもしれませんね。
00:16:45明らかにトレードオフが伴います。
00:16:47ええ、そしてそれに対して、もちろん私は「ソフトウェアエンジニアリングのすべてはトレードオフの管理だ」と言うでしょう。
00:17:01私たちは常に問題を正確に解決しているわけではなく、価値を提供するために新しいトレードオフのセットを選んでいます。
00:17:08だから、
00:17:09Turbo Packでの増分ビルドに関する設計目標を達成するために、
00:17:13私たちはこの増分リアクティブプログラミングモデルにすべてのチップを賭けました。
00:17:19そして、これはもちろんいくつかの非常に自然な結果をもたらしました。
00:17:23だから、
00:17:24手動で作成されたキャッシングシステムや面倒な無効化ロジックの問題を、
00:17:30私たちは実際に本当に解決したのかもしれません。
00:17:33その代わりに、私たちはいくつかの複雑なキャッシングインフラストラクチャを管理しなければなりません。
00:17:39そしてもちろん、それは私にとっては本当に良いトレードオフのように聞こえます。
00:17:42私は複雑なキャッシングインフラストラクチャが好きですが、私たちは皆、その結果を受け入れなければなりません。
00:17:48だから、最初のものはもちろん、このシステムのコアオーバーヘッドです。
00:17:54ご存知の通り、特定のビルドやHMRセッションで考えると、実際にはあまり多くのことを変更していません。
00:18:04だから、
00:18:04アプリケーション内のすべてのインポートとすべての解決結果の間のすべての依存関係情報を追跡しますが、
00:18:11実際に変更するのはごく一部だけです。
00:18:13だから、私たちが収集する依存関係情報のほとんどは、実際には決して必要とされません。
00:18:16だから、
00:18:17これを管理するために、
00:18:18私たちはこのキャッシングレイヤーのパフォーマンス向上に大きく焦点を当て、
00:18:23オーバーヘッドを削減し、
00:18:24私たちのシステムがより大規模なアプリケーションにスケールできるようにする必要がありました。
00:18:30そして次に来る最も明白なものは、単純にメモリです。
00:18:34ご存知の通り、キャッシュは常に根本的に時間とメモリのトレードオフです。
00:18:38そして、私たちのものもそこでは何も違いはありません。
00:18:41私たちのシンプルな目標は、キャッシュサイズがアプリケーションのサイズに線形にスケールすることです。
00:18:49しかし、繰り返しになりますが、オーバーヘッドには注意が必要です。
00:18:51次のものは少し微妙です。
00:18:54ご想像の通り、バンドラーには多くのアルゴリズムがあります。
00:18:58そして、その中にはアプリケーションに関するグローバルな何かを理解する必要があるものもあります。
00:19:03それは問題です。なぜなら、グローバルな情報に依存するたびに、どんな変更でもその操作を無効にする可能性があるからです。
00:19:10だから、これらのアルゴリズムをどのように設計し、増分性を維持できるように慎重に構成するかについて注意する必要があります。
00:19:17そして最後に、これは個人的な不満かもしれません。
00:19:24Turbo Packではすべてが非同期です。
00:19:27そして、
00:19:27これは水平スケーラビリティには素晴らしいですが、
00:19:29繰り返しになりますが、
00:19:30デバッグやパフォーマンスプロファイリングといった私たちの基本的な目標を損ないます。
00:19:38だから、皆さんの多くはChrome開発ツールなどで非同期デバッグの経験があるでしょう。
00:19:46そして、これは一般的にかなり良い経験です。
00:19:48常に理想的とは限りません。
00:19:49そして、LLDBを使ったRustは、はるかに遅れていると断言できます。
00:19:53だから、それを管理するために、カスタムの視覚化、計測、トレースツールに投資する必要がありました。
00:20:01そして見てください、またバンドラーではない別のインフラプロジェクトです。
00:20:07さて、では私たちが正しい賭けをしたかどうか見てみましょう。
00:20:11Vercelでは、非常に大規模な本番アプリケーションがあります。
00:20:17おそらく世界最大級の一つだと思いますが、実際にはわかりません。
00:20:21しかし、そこには約80,000のモジュールが含まれています。
00:20:23では、Turbo Packがそれに対してどのように機能するか見てみましょう。
00:20:26高速リフレッシュに関しては、Web Packが提供できるものを本当に圧倒しています。
00:20:32しかし、これは少し古いニュースです。
00:20:33開発用のTurbo Packはしばらく前からリリースされており、
00:20:36皆さんが少なくとも開発でそれを使っていることを本当に願っています。
00:20:39しかし、今日ここでの新しいことは、もちろんビルドが安定していることです。
00:20:42では、ビルドを見てみましょう。
00:20:44そしてここで、このアプリケーションにとってWeb Packに対する実質的な勝利を見ることができます。
00:20:49この特定のビルドは、実際には私たちの新しい実験的なファイルシステムキャッシングレイヤーで実行されています。
00:20:53だから、その94秒のうち約16秒は、最後にキャッシュをフラッシュしているだけです。
00:20:59そして、これはファイルシステムキャッシングが安定するにつれて改善に取り組んでいくものです。
00:21:04しかしもちろん、コールドビルドのことは、それがコールドであり、何も増分ではないということです。
00:21:07では、実際のウォームビルドを見てみましょう。
00:21:10コールドビルドからのキャッシュを使用して、これを見ることができます。
00:21:14だから、これは私たちが今日どこにいるかの一端です。
00:21:17このきめ細かいキャッシングシステムがあるので、
00:21:19実際にキャッシュをディスクに書き出し、
00:21:22次のビルドでそれを読み込み直し、
00:21:23何が変更されたかを把握し、
00:21:25ビルドを完了できます。
00:21:26さて、
00:21:27これはかなり良いように見えますが、
00:21:28皆さんの多くは
00:21:29「私は個人的に世界最大のNext.jsアプリケーションを持っているわけではないかもしれない」
00:21:33と考えているでしょう。
00:21:34では、より小さな例を見てみましょう。
00:21:37react.devのウェブサイトはかなり小さいです。
00:21:41また、Reactコンパイラなので、ある意味興味深いです。
00:21:44驚くことではありませんが、Reactコンパイラの早期導入者です。
00:21:47そして、ReactコンパイラはBabelで実装されています。
00:21:49そして、
00:21:50これは私たちのアプローチにとってある種の問題です。なぜなら、
00:21:52アプリケーション内のすべてのファイルについて、
00:21:53Babelに処理を依頼する必要があるからです。
00:21:55だから、そして根本的に言えば、私たち、あるいは私個人は、Reactコンパイラを高速化することはできません。
00:22:01それは私の仕事ではありません。
00:22:02私の仕事はTurbo Packです。
00:22:03しかし、いつそれを呼び出すべきかを正確に判断することはできます。
00:22:07高速リフレッシュ時間を見ると、この結果には少しがっかりしました。
00:22:13そして、その140ミリ秒のうち約130ミリ秒はReactコンパイラであることが判明しました。
00:22:18そして、Turbo PackとWeb Packの両方がそれを行っています。
00:22:22しかし、
00:22:22Turbo Packでは、
00:22:24Reactコンパイラがこの変更を処理した後、
00:22:26「ああ、
00:22:26インポートは変更されていない」と確認できます。
00:22:29それを出力に放り込んで、先に進みます。
00:22:31繰り返しになりますが、コールドビルドでは、このような一貫した3倍の勝利が見られます。
00:22:37そして、はっきりさせておきますが、これは私のマシンでのことです。
00:22:39しかし、繰り返しになりますが、コールドビルドには増分性はありません。
00:22:44そして、ウォームビルドでは、このはるかに良い時間が見られます。
00:22:47だから、繰り返しになりますが、ウォームビルドでは、すでにキャッシュがディスク上にあります。
00:22:52私たちが必要なのは、
00:22:53基本的に、
00:22:53開始したら、
00:22:54アプリケーション内のどのファイルが変更されたかを把握し、
00:22:57それらのジョブを再実行し、
00:22:58その後、
00:22:59以前のビルドからの他のすべてを再利用することだけです。
00:23:01だから、基本的な質問は、「もうターボですか?」です。
00:23:05はい。
00:23:06ええ、もちろんこれは基調講演で議論されました。
00:23:09Turbo PackはNext 16から安定版です。
00:23:12そして、私たちはNextのデフォルトバンドラーでもあります。
00:23:14だから、任務完了です。どういたしまして。
00:23:17しかし。(笑) (会場拍手)
00:23:23そして、
00:23:24基調講演でその「元に戻す」というものに気づいたなら、
00:23:27それは私がTurbo Packをデフォルトにしようとしたものです。
00:23:303回しかかかりませんでした。
00:23:31しかし、私が皆さんに伝えたいのは、やはりこれです。
00:23:35ご存知の通り、まだ終わっていません。
00:23:37パフォーマンスに関して、そしてファイルシステムキャッシングレイヤーの仕上げに関して、まだ多くのことが残っています。
00:23:42皆さん、開発環境でぜひ試してみてください。
00:23:44以上です。
00:23:46本当にありがとうございました。
00:23:47ぜひ私を見つけて、質問してください。
00:23:49(会場拍手) (アップビートな音楽) (アップビートな音楽)

Key Takeaway

Turbo Packは、増分リアクティブプログラミングモデルと自動化されたキャッシングにより、大規模なウェブアプリケーションのビルドおよび開発体験を劇的に高速化するVercelの新しいバンドラーです。

Highlights

Turbo Packは、増分性とキャッシングを核とした設計思想により、ビルドと高速リフレッシュのパフォーマンスを大幅に向上させます。

従来のバンドラーにおける素朴なキャッシングと無効化の課題を、シグナルに似た「増分リアクティブプログラミングモデル」で解決しています。

Turbo Packの「TurboTask」関数は、キャッシュ可能な作業単位であり、リアクティブな値(VC)を通じて依存関係を自動追跡・無効化します。

ファイルが変更されても、インポートなどの高コストな処理の結果が変わらなければ、その後のビルドステップをスキップし、高速なHMRを実現します。

大規模なアプリケーション(8万モジュール)において、Webpackと比較してコールドビルドで約3倍、ウォームビルドで圧倒的な速度向上を達成しています。

このアプローチには、複雑なキャッシングインフラの管理、メモリ使用量、グローバルアルゴリズムの設計、非同期デバッグの困難さといったトレードオフが存在します。

Turbo PackはNext.js 16から安定版となり、Next.jsのデフォルトバンドラーとして採用されていますが、パフォーマンスとファイルシステムキャッシングの改善は継続中です。

Timeline

自己紹介とTurbo Packの概要

スピーカーのルーク・サンドバーグはVercelのソフトウェアエンジニアで、Turbo Packを担当しています。彼は以前Googleで社内ウェブツールチェーンやTSXからJavaバイトコードへのコンパイラ開発に携わっていました。Vercelでの経験はGoogleとは大きく異なると感じており、本セッションではTurbo Packの設計上の選択とそれがパフォーマンス向上にどう寄与するかを説明します。特に、既存の素晴らしいパフォーマンスをさらに向上させるためのアプローチに焦点を当てます。

設計目標と増分性の重要性

Turbo Packの全体的な設計目標は、コールドビルドの重要性を認識しつつも、そもそもそれを経験しないようにすることです。バンドルパフォーマンス向上の鍵は「増分性」であり、すべてのバンドラー処理をキャッシュ可能にすることが目標です。これにより、変更を加えるたびに、アプリケーション全体の規模ではなく、変更の規模に応じた作業のみをやり直せばよくなります。例えば、多数のアイコンライブラリをインポートしても、開発者に優れたパフォーマンスを提供し続けることを目指しています。

素朴なキャッシングの課題

最もシンプルなバンドラーにキャッシュを追加する試みは、多くの問題に直面します。ファイル変更、シンボリックリンク、クライアント/サーバーバンドルでの同一ファイル利用、ASTのミューテーション、コンパイラ設定の欠落などが挙げられます。キャッシュキーに変換や設定を含めても、その複雑さ、開発者の保守性、キーのサイズといった課題が残ります。ファイル変更時の無効化ロジックも、コールバックをスレッド化してもバンドル全体を再実行する必要があり、真の増分性には至りません。最終的に、この素朴なアプローチは維持不可能であり、新しい機能の追加も困難であると結論付けられます。

理想的な解決策:シグナルとリアクティブプログラミング

理想的なバンドラーは、高コストな操作を簡単にキャッシュでき、開発者が手動でキャッシュキーや依存関係を追跡する必要がないべきです。ファイルや設定などの変更される入力を処理し、コールバックをスレッド化せずにリアクティブに再計算できる必要があります。さらに、マルチスレッドで高速な最新のアーキテクチャを活用することが求められます。これらの要件は「シグナル」のようなシステムに似ており、RustコンパイラのSalsaやAdaptonsといった学術文献からインスピレーションを得ています。Turbo Packでは「TurboTask」関数がキャッシュ可能な作業単位となり、リアクティブな値(VC)を通じて依存関係を自動追跡し、変更があった場合にのみ再実行することで、高速なリフレッシュタイムを実現します。

依存関係グラフとトレードオフ

Turbo Packの動作は、ノードの依存関係グラフとして捉えることができます。コールドビルドではすべてのファイルを読み込み、パースし、インポートを分析しますが、何かが変更された際には、構築された依存関係グラフを活用して無効化を伝播し、必要なTurboTask関数のみを再実行します。このシステムは非常に複雑で、約2,500種類のTurboTask関数と数百万のタスクが存在します。このアプローチにはトレードオフがあり、複雑なキャッシングインフラの管理、不要な依存関係情報の追跡によるコアオーバーヘッド、メモリ使用量、グローバル情報に依存するアルゴリズムの設計の難しさ、そして非同期処理によるデバッグの困難さが挙げられます。後者に対しては、カスタムの視覚化・計測・トレースツールへの投資が必要でした。

パフォーマンスベンチマークと結論

Vercelの8万モジュールからなる大規模な本番アプリケーションで、Turbo PackはWebpackを高速リフレッシュで圧倒し、コールドビルドでも約3倍の速度向上(94秒対280秒)を示しました。ウォームビルドでは、ディスクキャッシュを活用することでわずか1.6秒で完了します。react.devのような小規模なアプリケーションでも、Reactコンパイラ(Babel実装)の処理時間を除けば、コールドビルドで一貫して3倍の速度向上を達成しています。Turbo PackはNext.js 16から安定版としてリリースされ、Next.jsのデフォルトバンドラーとなりました。しかし、パフォーマンスのさらなる向上とファイルシステムキャッシングレイヤーの完成に向けて、開発はまだ継続中であり、開発環境での試用が推奨されています。

Community Posts

View all posts