このコンテンツは自動機械翻訳サービスによる翻訳版であり、皆さまの便宜のために提供しています。原本の英語版と異なる誤り、省略、解釈の微妙な違いが含まれる場合があります。ご不明な点がある場合は、英語版原本をご確認ください。
10月4日、独立系開発者のTheo Browne氏は、Cloudflare WorkersとVercel(AWS Lambda上に構築された競合コンピューティングプラットフォーム)のサーバー側のJavaScript実行速度を比較するために設計された一連のベンチマークを公開しました。最初の結果から、CPU負荷の高いさまざまなタスクにおいてVercel上のNode.jsよりもCloudflare Workersのパフォーマンスが3.5倍も悪いことが示されました。
私たちは、この結果に驚かされました。ベンチマークは、外部サービスを待たないCPU負荷の高いワークロードでJavaScriptの実行速度を比較するように設計されました。しかし、Cloudflare WorkersとNode.jsはどちらも、同じJavaScriptエンジンであるV8、Google Chrome製のオープンソースエンジンを使用しています。したがって、ベンチマークはそれぞれの環境で本質的に同じコードを実行していると予想されます。物理CPUは性能に差がありますが、最新のサーバーCPUは3.5倍近くは変わらないものです。
調査の結果、インフラストラクチャのチューニングの不適切から、各プラットフォームで使用されるJavaScriptライブラリの違い、テスト自体の問題まで、その格差の原因となったさまざまな小さな問題を発見しました。当社は1週間をかけて、これらの問題の多くに取り組みました。つまり、この1週間でWorkersはすべてのお客様のために改善され、高速になりました。当社ではなく、他のコンピューティングプロバイダーに影響を与えるいくつかの問題も修正しました。たとえば、Vercelでアルゴリズムを実行する関数がはるかに遅くなる問題などです。この記事では、その詳細をすべて掘り下げていきます。
重要なのは、元のベンチマークはCloudflareにおける課金可能なCPU使用率を表したものではなく、関連する問題がほとんどの典型的なワークロードに影響を与えたものでもありませんでした。格差のほとんどは、特定のベンチマーク方法論によって生じたものです。その理由を理解しましょう。
修正により、結果は期待していたものになります。
まだまだ課題はありますが、喜ばしいことに、これらの変更後、CloudflareはNext.jsに基づくものを除いて、すべてのベンチマークケースでVercelと同等のパフォーマンスを発揮しています。そのベンチマークでは、ギャップが大幅に縮小しており、この記事の後半で詳述するさらなる改善でこのギャップを解消できると考えています。
Theoには、改善できる点を強調してくれたことに感謝しています。それにより、すべてのお客様、そして当社のお客様でない多くのお客様に利益をもたらすことができます。
数値の比較を得るために、設計上の大きな変更を加えずにTheoのテストを実行したかったのです。ベンチマークケースは、Theoの最初のテストとほぼ同じですが、結果の正確性を高めるために、テストの実行方法にいくつかの変更を加えました。
彼らは、sfo1地域で実行されているVercelインスタンスに対して、サンフランシスコのWebpassインターネット接続で接続されたラップトップ上でテストクライアントを実行しました。結果を簡単に再現できるようにするために、代わりに、AWSのus-east-1データセンターで直接テストクライアントを実行し、iad1リージョン(同じビル内にあると理解しています)で実行されているVercelインスタンスを呼び出すことを選びました。これにより、ネットワーク遅延の影響を最小限に抑えることができると考えました。このため、Cloudflareの結果ではVercelの数値がTheoの数値よりも若干良い結果となりました。
私たちは、vCPUが2ではなく1であるVercelインスタンスを使うことを選びました。すべてのベンチマークがシングルスレッドのワークロードであり、2つ目のCPUを利用することはできません。VercelのCTOであるMalte Ublは、単一CPUインスタンスを使用してもこのテストでは違いがないとXで公に述べていました。そして実際、これは正しいことがわかりました。VercelとCloudflareのどちらもCPU時間に対して課金(iad1におけるVercelは0.128ドル/時間、Cloudflareはグローバルに0.072ドル)であるため、1vCPUを使用すると価格を考えるのも簡単になります。
テストでバグを修正するためにいくつかの変更を行い、プルリクエストを送信しました。これについては下記で詳しくみていきます。
Theoのベンチマークはさまざまなフレームワークをカバーしており、単一のJavaScriptライブラリが一般的な問題の原因になり得ることは明らかです。明らかに、まずWorkersランタイム自体を調べる必要がありました。そして、私たちは2つの問題を見つけました。バグではなく、チューニングとヒューリスティックの選択が、書かれているようにベンチマークとうまく連携することができないということです。
シャーディングとウォーム分離ルーティング:CPUスピードではなく、スケジューリングの問題
昨年、Cloudflareでは、トラフィックをより頻繁にウォーム分離へと送信するSmart Routingをリリースしました。これにより、大規模なアプリのコールドスタートを削減することができます。これは、Next.jsのように初期化要件が厳しいフレームワークにとって重要です。当初のポリシーは、何十億ものリクエストの遅延とスループットに対して最適化されていましたが、CPUが大量に使用するワークロードには、Node.jsのような他のプラットフォームでパフォーマンスの問題が起こるのと同じ理由で、あまり最適ではありませんでした。CPUが1つのリクエストに対して高価な操作を計算するのに忙しいとき、同じ分離に送信された他のリクエストは、その完了を待ってから先に進む必要があります。
システムは、ヒューリスティックを使用して、リクエストが互いにブロックされている場合を検出し、自動的により多くのアイソレートを立ち上げてそれを補完します。しかし、こうしたヒューリスティックは正確ではなく、Theoのテストで生成された特定のワークロードは、単一のクライアントから大量の高価なトラフィックが送信されることになり、既存のアルゴリズムでは不十分な役割を果たしました。その結果、ベンチマークは通常と予想されるよりもはるかに高い遅延(および遅延のばらつき)を示しました。
この問題の結果、ベンチマークは実際にはCPU時間を測定したものではないことを理解することが重要です。Workersプラットフォームの料金設定はCPU時間に基づいています。つまり、これは待機時間ではなく、実際にJavaScriptコードを実行する時間に基づいています。アイソレートが利用可能になるのを待つ時間がかかると、リクエストの処理時間が長くなりますが、待機中のリクエストに対するCPU時間として請求されることはありません。そうすれば、この問題によって請求に影響することはありませんでした。
ベンチマークを分析した後、アルゴリズムを更新し、継続的なCPU負荷の高いワークをより早く検出し、トラフィックをバイアスして、新しい分離のスピンアップを実現しました。その結果、異なるワークロードが適用される場合、Workersはより効果的かつ効率的にオートスケールを行うことができます。I/Oバウンドのワークロードは、すでに暖かい分離として個々に結束し、CPUバウンドは互いにブロックしないように指示されます。この変更はすでにグローバルに展開されており、すべての人に自動的に有効になっています。変更が展開されたときのグラフを見れば一目瞭然なはずです。
このスケジューリングの問題がベンチマークの格差の大半を占めていましたが、テスト中にコード実行のパフォーマンスに影響する小さな問題が見つかりました。
これらのベンチマークのフレームワークコードで発見したさまざまな問題は、結果に主な要因として、ガベージコレクションとメモリ管理の問題を繰り返し指摘しました。しかし、これらはNode.jsで動作する同じフレームワークでも問題になるだろうと予想されます。Workersでは何が起こっていたのか、なぜそれがこのような大幅なパフォーマンスの低下を引き起こしたのかを正確に確認するために、私たちは当社独自のメモリ管理構成を詳しく調べなければなりませんでした。
V8ガベージコレクションには、パフォーマンスに直接影響する調整可能なツールが膨大にあります。その1つが、「若い世代」の規模です。ここでは、新しく作成されたオブジェクトが最初に移動します。これは、コンパクトではないものの、短期間のオブジェクト用に最適化されたメモリ領域です。オブジェクトが数世代にわたって「ヤング空間」を行き来すると、古い空間に移動させられます。古い空間は、よりコンパクトですが、再利用するためにより多くのCPUを必要とします。
V8では、エンベッディングランタイムが新しい世代のサイズを調整することができます。実を言うと、我々はこれを実行したのです。2017年6月、Workersプロジェクトが開始されてからわずか2か月後、私たちは当時プロジェクトの唯一のエンジニアであった私、Kentonは、当時V8の推奨事項に従ってこの値を設定していましたメモリ512 MB以下の環境を対象としています。Workersのデフォルトの1つの分離あたり128MBの制限が設定されているため、これが適切であると考えました。
V8のガベージコレクション全体は、2017年以降劇的に変化しました。ベンチマークを分析すると、2017年には意味を成した設定が、2025年にはもはや意味をなさないことが明らかになり、V8の若い領域を過度に厳格に制限していることが判明しました。私たちの設定により、V8のガベージコレクションが必要以上に難しく、頻繁に動作するためでした。その結果、手動によるチューニングを後退し、V8が内部のヒューリスティックに基づいて、若い領域のサイズをより自由に選択できるようにしました。これはすでにCloudflare Workers上で稼働しており、メモリ使用量の増加はわずかでありながら、ベンチマークは約25%の上昇を実現しています。もちろん、メリットを受けるのはベンチマークだけではありません。すべてのWorkersが高速化されるべきです。とはいえ、ほとんどのWorkersにとってその差は遥かに小さいものです。
OpenNextをパフォーマンスのためにチューニング
プラットフォームの変更が問題のほとんどを解決しました。変更後、当社のテストでは、Next.jsのすべてのベンチマークについても、均等であることが示されました。
Next.jsは人気のあるWebアプリケーションフレームワークですが、従来はさまざまなプラットフォームでホスティングするためのサポートが組み込まれていませんでした。最近、OpenNextと呼ばれるプロジェクトがギャップを埋めるために立ち上げられ、Next.jsはCloudflareを含む多くのプラットフォーム上でうまく動作するようになりました。調査したところ、最適化の欠如や、パフォーマンスを向上させるその他の機会がいくつか見つかりました。これが、ベンチマークがWorkersでパフォーマンスが悪い理由の多くを説明するものです。
ベンチマークコードのプロファイリングを行った時、ガベージコレクションがタイムラインを支配していることに気づきました。リクエスト処理時間の10~25%がメモリの再利用に費やされていました。
そこで、当社が調査した結果、OpenNext、場合によってはNext.jsやReact自体が、プロセス処理中に最悪の時間帯に内部データバッファの不必要なコピーを作成することがよくあることが判明しました。たとえば、レンダリングパイプラインにPipeThthrough()操作が1つあり、実際に使用されているかどうかにかかわらず、50を下回らない2048バイトのバッファインスタンスが作成されています。
さらに、リクエストごとに、Cloudflare OpenNextアダプターが、ストリーミング出力データをレンダーからWorkersランタイムに引き渡されてユーザーに返す際に、すべてのチャンクを不必要にコピーしていることが判明しました。このベンチマークがリクエストごとに5 MBの結果を返すことを考えると、これは大量のデータがコピーされているということになります!
他の場所では、コレクションの総バイト数を取得するため以外の理由で、Buffer.concatを使って内部バッファインスタンスの配列がコピーおよび連結されていることがわかりました。つまり、getBody().lengthという形式のコードを見つけたのです。getBody()という関数は、バッファをどこにも保存することなく、多数のバッファを1つのバッファに連結して返します。つまり、その作業はすべて全体の長さを読み取るためだけに行われたのです。これは明らかに意図したものではなく、修正することは簡単でした。
これらの問題を修正するために、OpenNextで一連のプルリクエストを開き、その他の問題をホットパスで修正し、不要な割り当てやコピーをいくつか削除しました。
それだけではありません。当社はOpenNextのコードを繰り返し使用し、Workers上で実行される部分だけでなく、必要な場所に改善を加え続ける予定です。これらの改善の多くは、他のOpenNextプラットフォームにも適用されます。OpenNextの共通の目標は、お客様がコードを実行する場所にかかわらず、NextJSを可能な限り高速にすることです。
Next.jsのコードの多くは、Node.jsのバイトストリームのAPI。しかし、WorkersはWeb標準のStreams APIを好み、HTTPのリクエストとレスポンスの本文を表すためにそれを使用します。このためには、アダプターを使用して2つのAPI間で変換する必要があります。パフォーマンスのボトルネックを調査したところ、非効率なストリームアダプターが不必要に適用されている例が数多く見つかりました。これには、次の例が挙げられます。
const stream = Readable.toWeb(Readable.from(res.getBody()))
res.getBody()がBuffer.concat(chunks)を実行していて、蓄積されたデータチャンクが新しいバッファにコピーされ、続いてNode.js Streamにイテレーションとして渡されます。これは、有効化されたスタックを返すアダプターにラップされ、 ReadableStream。これらのユーティリティは有用な目的を果たしますが、Node.jsストリームとWebストリームの両方が独自の内部バッファを適用するため、これはデータバッファリングの悪夢となります。代わりに、次のようにすることができます。
const stream = ReadableStream.from(chunks);
これにより、追加のコピー、不要なバッファリング、または非効率な適応レイヤーをすべて通過することなく、蓄積されたチャンクから直接ReadableStreamを返します。
他の場所では、Next.jsとReactがReadableStreamを広く利用してバイトを通過させていることがわかりますが、作成されるストリームは、バイト指向ではなく値指向です。例えば、
const readable = new ReadableStream({
pull(controller) {
controller.enqueue(chunks.shift());
if (chunks.length === 0) {
controller.close();
}
}); // Default highWaterMark is 1!
完全に理にかなっています。しかし、ここに問題があります。チャンクがBufferまたはUint8Arrayインスタンスの場合、すべてのインスタンスはデフォルトで個別の読み取りになります。つまり、チャンクが1バイトのみの場合、または1000バイトの場合も、常に2回の読み取りとなります。これを合理的なハイウォーターマークを持つバイトストリームに変換することで、このストリームをより効率的に読み取ることができます。
const readable = new ReadableStream({
type: 'bytes',
pull(controller) {
controller.enqueue(chunks.shift());
if (chunks.length === 0) {
controller.close();
}
}, { highWaterMark: 4096 });
今では、ストリームは、異なるJavaScript値のストリームではなく、バイトのストリームとして読み取ることができ、個々のチャンクは内部で4096バイトのチャンクに結束できるため、読み込みをはるかに効率的に最適化することが可能です。ReadableStreamは、キューに入れられた個々のチャンクを一度に1つずつ読み取るのではなく、highWaterMarkに達するまで、pull()を繰り返し呼び出します。その場合、読み取りが一度に1つのデータチャンクをストリームに要求する必要がありません。
レンダリングパイプラインがバイトストリームを使用し、バックプレッシャーシグナルにもっと注意を払うことが最善策であるものの、当社の実装はこのようなケースをより適切に処理するために調整することができます。
つまり、できることはたくさんあります!OpenNextの実装には、いくつかの改善点があり、Cloudflare上で動作するようにするアダプターもいくつかあります。今後も調査と反復を行い続けていきます。すでにいくつかの修正を行い、すでに改善が見られます。また、近々、Next.jsとReactへのパッチの送信を開始し、アップストリームのさらなる改善を行い、理想的にはエコシステム全体に利益をもたらす予定です。
バッファの割り当てとストリームの他にも、プロファイルで測定値のように際立っている項目が1つあります。JSON.parse()は、Reviver関数を持つものです。これはReactとNext.jsの両方で使われており、当社のプロファイリングでは必要以上に大幅に遅いものでした。当社ではマイクロベンチマークを構築しましたが、最近、JSON.parse のre今際の引数は、標準がJSONソースコンテキストへのアクセスを提供するためにreviverコールバックに3番目の引数を追加した際にさらに遅くなることを確認しました。
リバース関数に慣れていない方も、アプリケーションはJSONの解析方法を効果的にカスタマイズすることができます。しかし、これには欠点もあります。この関数は、シリアル化されるArrayのすべての個別要素を含め、JSON構造に含まれるすべてのkey-valueペアで呼び出されます。TheoのNextJSベンチマークでは、1つのリクエストで100,000回を超える複数のリクエストが呼び出されることになります。
この問題は当社だけでなく、すべてのプラットフォームに影響しますが、私たちはそれを受け入れるつもりではないことを決めました。結局、WorkersランタイムチームがV8に貢献してくださっているのです!JSON.parse()のリバイバーを約33%高速化できるV8パッチをアップストリームしました。これは、V8のバージョン14.3(Chrome 143)からで、Cloudflare: Node.js、Chrome、Deno、エコシステム全体。Cloudflare Workersを使用していない場合、またはリバイバーの構文を変更しなかった場合、現在赤いパフォーマンスバーの下で問題が発生しています。
引き続き、フレームワーク作成者と協力して、ホットパスのオーバーヘッドを削減していきます。フレームワークに属する変更、一部はエンジンに属する変更、また一部はプラットフォームに属するものです。
私たちはエンジニアであり、自社の問題や広範なコミュニティの問題を解決したいと考えています。
Theoのベンチマークは、Cloudflare WorkersとVercelを比較した別の著者による別のベンチマークへの対応として投稿されたものです。算術アルゴリズムの呼び出しに焦点を当てた元のベンチマーク(サインとコサイン)を強固に囲い込むことも可能です。このベンチマークでは、Cloudflare WorkersはVercel上で動作するNode.jsの3倍の速さでパフォーマンスを発揮しました。
元のベンチマークの作成者は、Cloudflare Workersが単に速いという証拠としてこれを提供しました。彼らは反対し、私たちも統合しました。より速くなることを期待しています。私たちは自身では数学関数を実装しません。これらはV8に付属しています。結果を勝ち取ることはできなかったので、私たちは採用に踏み切りました。
Node.jsは、これらの関数の最新かつ最速のパスを使用していないことがわかりました。Node.jsは、clangまたはgccコンパイラーのいずれかで構築することができ、Workersよりも幅広いオペレーティングシステムとアーキテクチャをサポートするように書かれています。つまり、Node.jsのコンパイラーは、最も幅広いプラットフォームのサポートを提供するために、一部のものに対して最小公倍数を使用することになることが多いということを意味します。V8にはコンパイルタイムフラグが含まれており、一部の構成ではトリガー関数のより高速な実装が可能です。Workersでは、ほとんどの場合偶然に、このフラグがデフォルトで有効になっています。しかし、Node.jsでは違います。Node.jsでフラグを有効にするプルリクエストをオープンし、誰もが恩恵を受けることができるように、特にそれがサポートできるプラットフォームでは、誰もが恩恵を受けることができるようにします。
AWS LambdaとVercelがそれを採用できれば、この特定のギャップがなくなり、誰もがこれらの操作を高速化できると予想しています。Cloudflare Workersはすでに高速なtrig関数を使用しているため、この変更がお客様にメリットをもたらすことはありませんが、バグはバグであり、すべてを高速化したいと考えるためです。
最高のベンチマークにもバイアスやトレードオフがあります。現実世界のパフォーマンスを真に代表するベンチマークを作成するのは難しく、そうでないベンチマークの結果を誤解するのはあまりにも簡単です。私たちは、このテーマに対するPlanetscaleの取り組みが特に気に入りました。
これらの特定のCPUバウンドテストは、Webアプリケーションを表すのに理想的ではありません。TheOは、自身の動画でもそのことに言及しています。WorkersとVercel上の実世界アプリケーションのほとんどは、データベース、ダウンストリームサービス、ネットワーク、ページサイズに縛られています。エンドユーザーエクスペリエンスが重要です。CPUはその一部です。とはいえ、ベンチマークで遅いと示されれば、それを真剣に受け止めます。
ベンチマークは多くの実際の問題を発見し解決するのに役立ちましたが、ベンチマーク自体にもいくつかの問題が見つかり、スピードに差が生じる要因となりました。
ベンチマークはノートパソコン上で実行するように設計されており、そこからインターネット経由でCloudflareとVercelのサーバーにヒットします。クライアントから観測された遅延は、サーバー側のCPU時間の十分に近い近似値であると仮定しています。その理由は明白です。Theoが述べたように、Cloudflareは、タイミングサイドチャネル攻撃を防ぐために、アプリケーションが自身のCPU時間を測定することを許可していません。実際のCPU時間は終了後のログで確認できますが、その収集が大変な作業になるかもしれません。クライアントから時間を測定するのがもっと簡単です。
ただし、CloudflareとVercelは異なるデータセンターでホストされているため、それぞれへのネットワーク遅延がベンチマークの要因となる可能性があり、結果に影響を与える可能性があります。Cloudflareは、世界330都市以上に広がるロケーションでWorkerを実行できるため、通常、この効果はCloudflareに有利になります。これは、Cloudflareがお客様に最も近い場所を選ぶ傾向があるためです。一方、Vercelは通常、コンピューティングを中心的な場所に配置するため、その場所からの距離によって遅延が異なります。
当社独自のテストでは、この影響を最小限に抑えるために、Vercelインスタンスと同じデータセンターにあるAWS上のVMからベンチマーククライアントを実行しました。CloudflareはすべてのAWSロケーションに十分に接続されているので、これによってネットワーク遅延は排除されるべきだと考えています。デフォルトの選択肢として広く見られるため、AWSのus-east-1 / Vercelのiad1をテストに選択しました。いいとこ取りのどちらかでしょうか。
Cloudflareのサーバーはすべて同じではありません。積極的にリフレッシュしていますが、いつでも、本番環境では常に複数の世代のハードウェアが存在します。現在、これには第10世代、第11世代、第12世代のサーバーハードウェアが含まれます。
他のクラウドプロバイダーも同様です。新バージョンが利用可能になるたびに古いサーバーをすべて破棄するだけのクラウドプロバイダーはありません。
もちろん、新しいCPUはシングルスレッドのワークロードでも高速に動作します。20~30年前ほど大きな違いはありませんが、何もないわけではありません。このように、アプリケーションは、どのマシンに割り当てられているかによって、(若干の)幸運にも不運でもあるのです。
クラウド環境では、同じCPUでもマルチテナンシーが原因で、状況に応じてパフォーマンスが異なる可能性があります。アプリケーションが割り当てられているサーバーは、他の多くのアプリケーションも実行中です。AWS Lambdaでは、サーバーは数百のアプリケーションを実行しています。 Cloudflareでは、当社の極めて効率的なランタイムを使って、サーバーが何千ものサーバーを実行しているかもしれません。こうした「うるさい隣人」はアプリと同じCPUコアを共有しませんが、メモリ帯域幅などの他のリソースを共有してしまうことがあります。その結果、パフォーマンスにばらつきがあります。
こうした問題は、相関ノイズを生じさせることに留意することが重要です。つまり、再度テストを実行した場合、アプリケーションは以前と同じマシンに割り当てられたままである可能性が高くなります。これはCloudflareとVercelの両方に当てはまります。したがって、このノイズは単純に反復を実行するだけでは修正できません。Cloudflareでこのタイプのノイズを修正するには、さまざまなCloudflareデータセンターにヒットするために、そして異なるマシンにヒットするために、地理的に様々な場所からリクエストを開始する必要があります。しかし、これは多くの作業です。(Vercelでは、アプリケーションがVercelでマシンを切り替えるのに最適な方法についてはよくわかりません)。
NextJSベンチマークのCloudflareバージョンは、force-dynamicを使用するように構成されていませんでしたが、Vercelバージョンは構成されていました。これが好奇心旺盛な行動を引き起こしました。当社の理解では、「動的」ではないページは通常、構築時に静的にレンダリングされるべきであると考えられます。しかし、OpenNextでは、ページは引き続き動的にレンダリングされますが、同じページに対する複数のリクエストを同時に受信した場合、OpenNextはレンダリングを一度だけ呼び出すことになります。同じ分離に多くのリクエストを送信しないようにスケジューリングアルゴリズムを修正するために変更を行う前は、この動作がこの問題にある程度対抗できたかもしれません。Theoは、Cloudflareのバージョンでフォースダイナミックを無効にしたと報告しています。
しかし、皮肉なことに、そのスケジューリングの問題を解決したら、「静的」レンダリング(つまり、フォースダイナミックを有効にしないなど)は、他の理由でCloudflareのパフォーマンスを低下させます。OpenNextが「キャッシュ可能な」ページをレンダリングすると、レスポンス本文のストリーミングが禁止されるようです。これは、ベンチマーククライアントのプロパティとの相互作用が不十分でした。リクエスト/レスポンスの合計時間ではなく、サーバー初期応答時間(TTFB)を測定していました。Vercelでのテストのように動的モードで実行する場合、ページ全体がレンダリングされる前に、最初のバイトがクライアントに返されます。残りのレンダリングはバイトがストリームアウトする際に行われます。しかし、非動的モードのOpenNextでは、バイトがクライアントに返される前に、悪意のあるペイロード全体が巨大なバッファにレンダリングされました。
ベンチマーククライアントのサーバー初期応答時間(TTFB)動作のため、動的モードではベンチマークは実際にはページを完全にレンダリングするのに必要な時間を測定しません。Vercelの可観測性ツールが、ベンチマーク自体の報告よりも多くのCPU時間が費やされていることに気づいた時、疑いが生じました。
1つの選択肢は、ベンチマークを変更してTTLBを使用することでした。つまり、最後のバイトが受信されるまでタイマーを停止するまで待つのです。ただし、これにより、ベンチマークはさらにネットワークの違いに影響を受けることになります。レスポンスは2MBから15MBと非常に大きいため、結果はプロバイダーへの帯域幅によって異なる可能性があります。確かに、これはCloudflareに有利になる傾向がありますが、テストの目的は帯域幅ではなく、CPU速度を測定することであるため、不当な優位性となります。
Cloudflareバージョンのテストでも、force-dynamicを使用するように変更したところ、Vercelのバージョンと一致させ、ストリーミングの動作が一致し、リクエストが公正になりました。つまり、どちらのバージョンもHTMLにページ全体をレンダリングするコストを測定していないものの、少なくとも現在では同じものを測定しています。
余談ですが、当初の動作により、OpenNextがレンダリングリクエストの重複を排除するために使用するComposable キャッシュの実装に、いくつかのパフォーマンスのボトルネックがあることが判明しました。これらの修正がこの特定のベンチマークの数値に影響を与えることはありませんが、これらの部分の改善にも取り組んでいます。
React SSRベンチマークには、より基本的な設定エラーが含まれていました。Reactは、環境変数NODE_ENVを検査して、環境が「本番」環境か開発環境かを判断します。Vercelを含む多くのNode.jsベースの環境は、本番環境でこの変数を自動的に設定します。OpenNextなど、多くのフレームワークでは、本番環境のWorkersにも自動的にこの変数を設定します。しかし、React SSRベンチマークは、下位レベルのReact APIsに対して書かれたものであり、フレームワークは一切使用しません。この場合、NODE_ENV変数はまったく設定されていませんでした。
そして、残念ながら、NODE_ENV が設定されていない場合、React はデフォルトで「開発モード」になります。これは、追加のデバッグチェックを含むモードであるため、本番モードよりもはるかに低速です。その結果、Workersの数値は本来よりもはるかに悪いものでした。
おそらく、Workersが特にNode.jsの互換性が有効になっている場合、デプロイされたすべてのWorkerに対して自動的にこの変数を設定することは理にかなっているかもしれません。将来的にはこれを行うことを検討していますが、今のところは直接設定するようにテストを更新しました。
Workersランタイムの改善はすでにすべてのWorkerで利用可能であるため、何も変更する必要はありません。多くのアプリでは、コンピューティング負荷の高いルートで高速かつ安定したテール遅延が実現し、バースト時のジッターも少なくなります。ガベージコレクションが改善された場所では、一部のワークロードではCPU秒数が少なく請求されます。
また、Theoには、OpenNextの改善点や他のテスト修正を適用して更新するプルリクエストも送信しました。
しかし、これで終わりではありません。Vercel上のOpenNextとNext.jsのギャップを解消する作業はまだありますが、他のベンチマークの結果を見れば明らかです。また、スケジューリングアルゴリズムのさらなる改善にも計画しており、リクエストが互いにブロックすることがほとんどありません。当社はV8、そしてNode.jsの改善を続けていきます。Workersチームは、各プロジェクトで複数のコアコントリビューターを採用しています。当社のアプローチはシンプルです。オープンソースのインフラストラクチャを改善して誰もが高速化し、プラットフォームがその改善を最大限に活用できるようにするのです。
そしてもちろん、今後もこのような問題を自社で検出できるように、より多くのベンチマークを書いていく予定です。Workersが遅いことを示すベンチマークを持っている場合は、リプロでお知らせください。プロフィールを確認し、アップストリームできるものを修正し、学んだことを共有していきます!