【翻訳】Prettier's CLI: A Performance Deep Dive

2023/12/01に公開

この記事は Fabio 氏が Prettier 公式ブログに寄稿してくれた Prettier's CLI: A Performance Deep Dive を許可をもらって翻訳したものです。このブログ記事は長く、まじめに翻訳する時間がなかったので、ChatGPT でサクッとやりました。変なところがあったらコメントで教えてください。

先日の記事で紹介したように、Prettier には Biome Formatter という強力な競合が現れました。Biome は Rust で書かれており非常に高いパフォーマンスを発揮します。しかし、実は今のところ Prettier のパフォーマンスを改善しようと真剣に取り組んだ人は(おそらく)一人もおらず、現在の Prettier の JavaScript の実装にはパフォーマンス改善の余地がたくさん残されています。個人的にも、コードフォーマッタに求められるパフォーマンス要件程度なら、JavaScript(V8)で達成できないわけはないだろうと思っています。

Fabio 氏はその中でも CLI に焦点をあてて、パフォーマンスの改善に取り組んでくれました。


こんにちは、私はFabioです。この度、PrettierチームによってPrettierのコマンドラインインターフェース(CLI)の高速化のために雇われました。この投稿では、私が発見した最適化、それらを見つける過程、現在のCLIと新しいCLIを比較したエキサイティングな数字、そして次に最適化できる可能性のあるものについて見ていきます。

インストール

Prettierの新しい進行中のCLIはリリースされたばかりで、今すぐインストールできます:

npm install prettier@next

これは大部分が後方互換性を持っています:

prettier . --check # 以前と同じように、ただし速く

問題が見つかった場合は、環境変数を使って一時的に古いCLIを使用することができます:

PRETTIER_LEGACY_CLI=1 prettier . --check

また、npxを使って試すこともできますが、npx自体はかなり遅いです:

npx prettier@next . --check

目標は、約100%後方互換性を持たせ、将来の安定版リリースで現在のCLIを置き換えることです。

概要

PrettierのCLIは上の図のように大まかに動作します:

  1. ファイルに対して実行したい何らかのアクションがあります。例えば、適切にフォーマットされているかどうかをチェックすることです。
  2. このアクションを実行するために、実際にすべてのファイルを見つける必要があります。
  3. .gitignoreファイルと.prettierignoreファイルを解決し、これらのファイルの中に無視すべきものがあるかどうかを判断します。
  4. .editorconfigファイルを解決し、それらのファイルに対するEditorConfig固有のフォーマット設定を行います。
  5. .prettierrcファイルなど、約10以上のPrettier固有のフォーマット設定ファイルを解決します。
  6. 各ファイルがそのフォーマット設定に一致しているかどうかをチェックします。
  7. 最後に、ターミナルに何らかの結果を出力する必要があります。

CLIのアーキテクチャを高レベルで見たとき、主に3つの観察があります:

  1. 作業量は対象ファイルの数に比例しますが、CLIの実行間でほとんどのファイルは変わらないため、以前の実行で行われた作業を覚えておくことができれば、現在の実行でのほとんどの作業をスキップできます。
  2. リポジトリには何千ものフォルダがあり、それぞれのフォルダに設定ファイルがあるため、解決すべき設定ファイルが膨大な数になる可能性があります。しかし、ほとんどの場合、リポジトリにはたった1つか少数の.editorconfigファイルしか含まれていないため、これはそれほど高価なものではないはずです。
  3. これは特に洞察に富んだ観察ではありませんが、プログラムが行っているすべてが必要であり、行われているすべてが効率的であれば、その結果としてプログラム自体も効率的に実行されるはずです。したがって、私たちは可能な限り不要な作業をスキップしようとし、スキップできないものは効率的に行おうとします。

これらの観察から、パフォーマンスを最初から念頭に置いて書き直す方が、パッチを当ててパフォーマンスを向上させるよりもしばしば簡単であるため、Prettierの新しいCLIを一から書き始めました。

この投稿を通じて、Babelのモノレポを測定に使用します。これは良い種類のベンチマークを提供し、実際にかなり大きなモノレポでの改善がどのようなものかを感じることができるでしょう。

ファイルの迅速な検索

まず最初に、フォーマットの対象とするファイルを見つける必要があります。Prettierの現在のCLIでは、fast-globを使用しており、そのコードは次のようになります:

import fastGlob from "fast-glob";

const files = await fastGlob("packages/**/*.{js,cjs,mjs}", {
  ignore: ["**/.git", "**/.sl", "**/.svn", "**/.hg", "**/node_modules"],
  absolute: true,
  dot: true,
  followSymbolicLinks: false,
  onlyFiles: true,
  unique: true,
});

これをBabelのモノレポで実行すると、約30,000の合計ファイルのうち、約17,000のファイルが私たちの glob に一致するのを見つけるのに約220ミリ秒かかることがわかります。これは、Babelのモノレポに13,000以上のフォルダーが含まれていることを考えると、妥当な速さのようです。

簡単なプロファイリングで、見つかったファイルが ignore glob のいずれかと一致するかどうかを確認するのにかなりの時間が費やされていることがわかります。これらは内部的に正規表現に変換され、一つずつ照合されるようです。そこで、glob を単一のものに統合してみました:"**/{.git,.sl,.svn,.hg,node_modules}"、しかし何らかの理由で内部的に再び分割されるようなので、何も変わりませんでした。

ignore glob 全体をコメントアウトすると、ほとんど同じファイルを約180ミリ秒で見つけることができます。これは約20%の時間の短縮です。したがって、これらの glob をより速い方法で一致させることができれば、時間を少し短縮できるかもしれません。

ここでfast-globが行っているかなりクールな最適化に触れるのが良いでしょう。もし私たちの glob がpackages/**/*.{js,cjs,mjs}のようなものなら、その始まりが完全に静的であることがわかります。実際に私たちが行いたいのは、packagesフォルダー内で**/*.{js,cjs.mjs}を検索することです。この最適化は、他のフォルダーに興味がない場合に重要になります。なぜなら、それらのファイルはまったくスキャンされないからです。

そこで私は考えました: 新しいバージョンの ignore glob も基本的にこれまでのものと同じですが、静的な部分は終わりにあります。そこで、静的な終わりも利用できる glob でファイルを見つけるための別のライブラリを書きました。それでファイルを見つけるための同等のコードは次のようになります:

import readdir from "tiny-readdir-glob";

const { files } = await readdir("packages/**/*.{js,cjs,mjs}", {
  ignore: "**/{.git,.sl,.svn,.hg,node_modules}",
  followSymlinks: false,
});

そして、それは同じファイルを約130ミリ秒で見つけます。無視する glob をコメントアウトして、それがどれだけのオーバーヘッドを加えるかを確認するために、それはおおよそ同じ時間を要するようです。したがって、このシナリオではそのコストを測定するのが難しいほど十分に最適化されました。

いくつかの理由で、私たちが予想したよりも速くなりました:

  1. ignore glob 以外に、メインの glob の**/*.{js,cjs,mjs}部分も最適化されました。
  2. glob は、私たちが検索を開始したルートフォルダーからのファイルの相対パスに対して一致します。そのため、そこにはいくつかのpath.relativeの呼び出しがあります。しかし、私たちの glob が**/.gitのように見える場合、相対パスを計算するかどうかは関係ありません。私たちは効果的にその文字列の終わりを子パスで探しているので、私はそれらのpath.relativeの呼び出しを完全にスキップしています。
  3. Nodeのfs.readdir APIは、両方のライブラリ内でディレクトリをスキャンするために内部的に使用され、私たちに見つかったファイルとフォルダーの名前を与えます。これは私たちが実際に欲しい絶対パスではありませんが、親のパスと名前をパスセパレータで結合することによって、より手動的に生成できます。これはpath.joinを呼び出すよりも少し速くなります。

さらに、この新しいライブラリは約90%小さく、65kbの最小化されたコードが不要になり、CLI全体の起動が速くなります。Babelのものよりはるかに小さいリポジトリでは、新しいCLIは古いものがfast-globの解析を終えるまでにすべての目標ファイルを見つけるかもしれません。

このCLIの一部を書き換えることは、得られたスピードアップの種類に対して少々重手に見えるかもしれませんが、実際のところその再作成の主な理由は別のものです。ファイルをすべて見つけるのに最初から半秒以下しかかからないため、CLIの速度を数秒間スピードアップすることは不可能に思えますが、全体としてCLIにはフォーマットするファイルだけでなく、設定ファイルも見つける必要があります。そして、私たちの glob に一致しなかったとしても、見つかったすべてのファイルとフォルダーを知ることは、後で数秒を削るのに非常に価値のある情報です。tiny-readdir-globはその情報をほぼ無料で提供できるため、それだけのために書く価値がありました。

このセクションからのおそらく興味深いポイントをまとめると:

  1. できれば、Prettierに検索する拡張子を常に伝えるようにしてください。例えば、packages/**/*.{js,cjs,mjs}のような glob を使用します。このシナリオでpackages/**/*や単にpackagesを使用した場合、13,000もの追加ファイルを処理する必要があり、Prettierがそれらを後で破棄するのにより多くのコストがかかります。
  2. 既に最適化されているライブラリでも、時間をかけて調べることができれば、最適化すべき点やパフォーマンスのための特別なケースが常に残されています。
  3. 捨てられている情報、または再構築するのにある程度のコストがかかる情報について考えることは価値があります。ここで、glob ライブラリは仕事をするために見つかったファイルとフォルダーを知っている必要があり、その情報を呼び出し元に公開することで、場合によっては追加のパフォーマンスを基本的に無料で解放することができます。

さらにスピードアップする方法についての推測:

  1. これはNodeのfs.promises.readdirのパフォーマンス、または見つかった各フォルダーに対するPromiseの作成に関連するオーバーヘッドによるボトルネックのようです。そのAPIのコールバックスタイルのバージョンを使用することや、Node自体の最適化の機会を探る価値があるかもしれません。

設定ファイルの迅速な解決

これは新しいCLIの背後にある最も影響力のある最適化かもしれません。基本的には、各フォルダーで設定ファイルをできるだけ早く見つけ、各フォルダーが設定ファイルを含むかどうかを一度だけチェックし、見つかった設定ファイルを一度だけ解析することに関するものです。

現在のCLIの大きな問題は、解決された設定をファイルパスではなくフォルダーパスでキャッシュしていないことです。例えば、Babelのモノレポにはフォーマットするための約17,000のファイルがありますが、リポジトリ全体で1つの.editorconfigファイルしかありません。このファイルは一度だけ解析されるべきですが、代わりに約17,000回解析されました。さらに、これらの約17,000のファイルが同じフォルダーの下にあると想像すると、そのフォルダーは.editorconfigファイルを含んでいるかどうか約17,000回問い合わせされたので、実際には、フォーマットするファイルがフォルダーの中に深くネストしているほど、CLI全体が遅くなりました。

この問題は主に2つのステップで大きく解決されました:

  1. 解決された設定ファイルはフォルダーパスでキャッシュされるようになり、各フォルダーが含むファイルの数や、それらがフォルダーの中でどれだけ深くにあるかはもはや重要ではありません。
  2. 前のセクションでリポジトリの中のすべてのファイルを知っているため、約15のサポートされている設定ファイルのそれぞれを含むかどうかを尋ねるフォルダーが基本的に0回になりました。これにより、ファイルシステムに尋ねるよりもはるかに高速な検索が可能になります。これは、ほとんどの小規模なリポジトリではあまり問題になりませんが、例えばBabelの場合は約13,000のフォルダーがあり、13,000 x 15のファイルシステムチェックは十分に速く追加されます。

それでは、サポートされている各種類の設定がどのように解決されるかを詳しく見ていきましょう。

EditorConfig設定の解決

前のステップでリポジトリ内のすべての.editorconfigファイルが解析され、任意のターゲットファイルに対して関連するものを定数時間で取得できると仮定すると、私たちが今したいことは、基本的に各ターゲットファイルに対してそれらを単一の設定オブジェクトにマージすることです。

ただし、このアイデアはすぐに無効になります。なぜならeditorconfigパッケージはこれを行うための機能を提供していないからです。最も近いものはparseFromFilesと呼ばれるもののようですが、これは廃止予定であり、私たちが望んでいることを行うようですが、それは文字列としての設定を望んでいるため、おそらくそれぞれの呼び出し後にそれらを自分で解析するでしょう。これは最初に避けたいことであり、これらを一度だけ解析したいだけです。

したがって、このパッケージはPrettierのニーズに合わせて書き直されました。tiny-editorconfigは、私たちが必要とする正確なresolve機能を提供し、設定ファイルを見つけるためのロジックは私たちに任されています。これは私たちがこれらのファイルをカスタムな方法でキャッシュする必要があるため、望ましいことです。

それを行う間に、その背後にあるINIパーサーも書き直し、それは約9倍速くなったようです。ほとんどのリポジトリには1つの.editorconfigファイルしかないため、それほど重要ではないはずですが、私はこれらの小さなパーサーを書くのが好きで、もしあなたのリポジトリに何千もの.editorconfigファイルがあるなら、余分なパフォーマンスブーストに気付くでしょう!

さらに、この新しいライブラリは約82%小さくなり、50kbの最小化されたコードが削除され、CLI全体の起動が速くなりました。また、tiny-readdir-globが使用するのと同じglobライブラリを使用しているのに対し、現在のCLIではfast-globeditorconfigが異なるものを使用しているため、実際にはそれ以上のコードが効果的に削除されました。

以前はBabelのモノレポのこれらのファイルを解決するのに数秒かかりましたが、今は約100ミリ秒です。

これをさらに高速化する方法についての推測:

  1. いくつかの(ほとんどの?)ケースでは、私たちが遭遇する可能性のある任意のファイルパスに対して、これらの設定ファイルを事前に解決することが可能であるべきです。例えば、それらのglobに依存して、最大で3つの可能な解決された設定しか結果として得られない可能性があります。1つはglobに一致しないファイル用、もう1つは**/*.jsglobに一致するファイル用、もう1つは**/*.mdglobに一致するファイル用です。これを実装するのは少し複雑ですが、実際にどの程度の速度向上が得られるかは不明ですが、将来的に考えるべきことです。

Prettier設定の解決

.prettierrcファイルのようなPrettier固有の設定については、見つかった設定ファイルをすべて解決し、任意のターゲットファイルに対して定数時間で取得できると仮定しています。

基本的には、EditorConfig固有の設定であった状況と同じです。したがって、基本的に同じことを行いますが、今回はCLI自体の内部に設定のマージロジックをハードコーディングします。なぜなら、これに対する独立したパッケージを作ることはエコシステムにとってほとんど無用であると思われるからです。

ここで将来に向けて考慮すべき主な点は、私の意見では次のとおりです:

  1. さまざまな設定ファイルがサポートされています。Babelのモノレポでは、これは最初のステップで作成した既知のパスのオブジェクトで約150,000回のルックアップに変換されます。これは超高価ではありませんが、無料でもありません。この数を大幅に減らすことができれば、物事を少し速めることができます。
  2. これらの設定ファイルを解析するために必要な一部のパーサーは比較的高価です。json5パーサーは、私が知っている最小のJavaScript用JSONC(コメント付きJSON)パーサーよりも約100倍のコードを必要とし、場合によっては解析速度も約50倍遅いです。サポートされる形式が少なければ、その結果としてCLIはより軽量になります。
  3. たとえば.prettierrc.json5という名前のファイルがリポジトリ内のどこかで見つかったかどうかを一度だけチェックできれば、これらの設定ファイルに対するチェックの数を10分の1に減らすことができます。なぜなら、その名前のファイルがリポジトリ内のどこかで見つからなければ、Babelの約13,000のフォルダーのそれぞれにそれがあるかどうかを尋ねる必要はないからです。私たちが使用しているglobライブラリが無料で提供できるもう一つの貴重な情報は、すべての既知のファイル名のリストです。

ignore の設定の解決

最後に、見つかったファイルのうちどれを無視するかを理解するために、.gitignoreファイルと.prettierignoreファイルを解決する必要があります。ここでも、見つかったものをすべて解決し、任意のターゲットファイルに対して定数時間で取得できると仮定しています。

ここでは大きな最適化は行っていません。主にnode-ignoreにファイルを無視するかどうかをチェックするための関数を作ってもらうように頼んでいます。

しかし、小さな最適化としては、path.relativeの呼び出しと、場合によってはignore関数自体の呼び出しをスキップしています。無視ファイルは無視ファイルが存在するフォルダーからの見つかったファイルの相対パスとほぼ一致します。そして、私たちが扱っているすべてのパスが正規化されていることを知っているので、ターゲットファイルの絶対パスが各無視ファイルが存在したフォルダーの絶対パスで始まっていない場合、そのファイルは無視ファイルが管理するフォルダーの外に存在するので、node-ignoreが私たちのために作ったignore関数を呼び出す必要はありません。

ただし、これらのファイルのglobと見つかったファイルを一致させるのにかなりの時間がかかっているようです。数百ミリ秒で数千のファイルがかかることがあります。これは、これらのファイルに多くのglobがあり、一致させるための多くのファイルがあるためで、これらが掛け合わされると最悪の場合に試みられるglobの一致の大体の数になります。

ただし、.gitignoreファイルと.prettierignoreファイルの良い点は、それらによって無視されるファイルを処理するために必要な時間よりも、それらを解析し、ファイルと一致させるために費やされる時間がしばしば少ないことです。

これをさらに高速化する方法についての推測:

  1. これらのglobのほとんどは、一つの複雑なglobにまとめられ、エンジンによって一度に一致させることができるかもしれません。私たちは正確な一致したglobがどれであるかを知ることには興味がなく、どれかが一致したかどうかだけが重要です。
  2. おそらくglobは異なる順序で実行されるかもしれません。安価で広範囲なglobを最初に実行することで、平均してglobの一致にかかる時間を少なくすることができるかもしれません。ただし、ほとんどの見つかったファイルが無視されない場合は、これは違いを生み出しません。
  3. おそらくキャッシュで一致したかどうかのファイルパスを記憶することができますが、キャッシュなしでもかなり高速化できるように感じます。

キャッシュ

この時点で、すべてのファイルを見つけ、すべての設定を解析しました。残された作業は、各ターゲットファイルの実際の潜在的に高価なフォーマット処理を行うことです。ここでキャッシングが登場します。

現在のCLIはキャッシングの形態をサポートしていますが、これはオプトインであり、明示的な--cacheフラグが必要であり、前回の実行でファイルが適切にフォーマットされていないことを記憶していません。適切にフォーマットされている場合のみ記憶しているため、一部のケースで無駄なオーバーヘッドが発生する可能性があります。これは、フォーマットされていないファイルが、前回の実行からフォーマットされていないことを覚えておけばよかったのに、再度フォーマットされてフォーマットされているかどうかをチェックするためです。

ここで私たちがしたいことは、ファイルがフォーマットされたかどうかを記憶することにより、できるだけ多くの作業をスキップし、合理的に小さなキャッシュファイルを生成し、キャッシングメカニズム自体であまりオーバーヘッドを導入しないことです。

新しいCLIではオプトアウトキャッシングが採用されているため、キャッシングは常にオンになっており、明示的に--no-cacheフラグで無効にしない限り利用されます。これにより、その利点がデフォルトでより多くの人々に届くことになります。また、キャッシュは、すべてを考慮に入れるため、デフォルトでオンになっています。したがって、キャッシュがCLIに実際に正しくないことを伝えることは現実的ではありません。以下のいずれかが変更されると、キャッシュまたはその一部が自動的に無効になります:Prettierのバージョン、すべての解決されたEditorConfig/Prettier/Ignore設定ファイルとそのパス、CLIフラグ経由で提供されるフォーマットオプション、各ファイルの実際の内容、および各ファイルのファイルパス。

ここでの主なトリックは、キャッシュが各ファイルの解決されたフォーマット設定に_直接_依存しないようにすることです。なぜなら、それには各ターゲットファイルに対して設定ファイルをマージし、結果のオブジェクトをシリアライズし、それをハッシュ化する必要があり、それは我々が望むよりも高価になる可能性があるからです。

新しいCLIが代わりに行っていることは、見つかったすべての設定ファイルを解析し、それらをシリアライズしてハッシュ化することです。これは、後でフォーマットする必要があるファイルの数に関係なく、一定の時間で行うことができ、キャッシュファ

イルに設定ファイルを_間接的に_考慮するために1つのハッシュを記憶するだけで済みます。これは安全です。なぜなら、設定ファイルへのパスとその内容が変わらなければ、前回の実行から同じパスを持つ任意のファイルは必然的に同じ解決されたフォーマット設定オブジェクトでフォーマットされるからです。唯一の潜在的な問題は、それらの設定ファイルを解析するために使用される任意の依存関係が実際にバグがある場合ですが、最悪のシナリオではPrettier自体のバージョンがバグのある依存関係と一緒に上げられます。

いくつかの数字を挙げると、現在のCLIはキャッシュなしでBabelのモノレポを約29秒でチェックしますが、新しいCLIはキャッシュなしで同じことを約7.5秒で行います。キャッシュファイルが有効で存在する場合でも、現在のCLIは依然として約22秒必要ですが、新しいCLIは約1.3秒しか必要としません。この数字は将来のさらなる最適化で半分に減らすことができるかもしれません。

この長い投稿から覚えておくべきことは、CLIをできるだけ高速にするためには、キャッシュファイルを記憶することが必要です。キャッシュファイルはデフォルトで./node_modules/.cache/prettierの下に保存され、--cache-location <path>フラグを渡すことでその場所をカスタマイズできます。もう一度言います:パフォーマンスが重要なシナリオでは、ここを高速化するための最も大きなことは、実行間でキャッシュファイルを記憶することです。

さらに高速化する方法についての推測:

  1. 最適化の機会は、Nodeでのハッシュ計算を高速化することです。Bunで同じハッシュを計算するのは約3倍速いので、確実に最適化の余地があるはずです。私はそれをNodeに報告しましたが、まだそれに対処するPRは提出されていません。それは複雑なようです。
  2. 解析された設定ファイルもキャッシュされる可能性があります。ただし、そのハッシュを記憶するだけでなく、通常はそれらのファイルが少数しかないので、それほど重要ではないかもしれません。
  3. より多くのコードが削除されたり、遅延ロードされたりすることで、完全にキャッシュされたパスをもう少し高速化できるかもしれません。

フォーマット処理

パイプラインの最後にほぼ到達しました。フォーマットする必要があるファイルがわかり、実際に行うだけです。

コアのフォーマット機能自体の最適化についてはあまり見ていません。なぜなら、少数のファイルに対してはすでにかなり速いと思われるし、CLIの遅延のほとんどは設定の非効率的な解決と過去の作業の記憶がないことから来ているようだったからです。しかし、次に検討すべき主な事項の一つかもしれません。ただし、簡単なプロファイリングを行ったところ、少なくとも明らかな最適化の機会は見つかりませんでした。

しかし、他のいくつかのことを試しました。

まず、作業は並列化できます。すべてのコアを使用してファイルを並列にフォーマットするための新しい--parallelフラグがあります。また、手動で微調整するために使用するワーカーの数を設定する--parallel-workers <int>フラグもあります。--parallelフラグを設定した10コアのコンピューターでは、Babelのモノレポをチェックするのにかかる時間は約7.5秒から約5.5秒に短縮されますが、これは特に印象的ではありません。なぜそれ以上のスケーリングが得られないのか分かりませんが、いずれ詳しく調査したいと思っています。もっと大きなリポジトリや何百ものコアを持つCIマシンがありますので、試してみてください。それがあなたのユースケースにとって大きな違いをもたらすかもしれません。

最後に、Prettierのformat関数を@wasm-fmt/biome_fmtBiomeのformat関数をWASMにコンパイルしたもの)に素早く置き換えてみると、Babelのモノレポをチェックするのに約3.5秒、並列化すると約2.2秒かかりました。Prettier自身のフォーマッターで見た数字の約2倍の改善です。Biomeのformat関数がネイティブのNodeモジュールにコンパイルされた場合、改善はさらに大きくなる可能性がありますが、確信はありません。

さらに高速化する方法についての推測:

  1. コアフォーマットにはあまり手を付けていませんが、最適な状態よりも少なくとも2倍遅いようです。しかし、そこに到達するためには大きな作業が必要かもしれません。しかし、改善の余地は確実にあります。
  2. --parallelフラグは将来的にデフォルトで有効にされるべきかもしれません。ただし、フォーマットするファイルが少なく、供給するコアが多い場合、各コアに少数のファイルを供給することになり、実際には少し遅くなる可能性があります。

ターミナルへの出力

最後に、CLIが実行するように指示されたコマンドの結果をターミナルに出力するだけの最終段階です。

ここでは多くの作業はありませんでしたが、いくつかの最適化が可能でした:

  1. まず、多くの小さなファイルをフォーマットする場合のように、現在のCLIは非常に迅速に現在フォーマットしているファイルのパスを出力し、フォーマットが完了した直後にそれを削除します。何千ものファイルに対してこれを行うことは実際には驚くほど高価です。なぜならconsole.logの呼び出しは同期的でメインスレッドをブロックするためです。さらに、16ミリ秒以内にこれを100回行ってもあまり意味がありません。なぜなら、その時間内に画面がたった1回か2回しか更新されないかもしれないからです。ログされた内容はほとんど見られませんでした。現在の新しいCLIは、現在フォーマット中のファイルをログに記録しないため、場合によっては数百ミリ秒を短縮します。
  2. 最後に、現在のCLIは、フォーマットされるファイルの数に応じてconsole.logを何千回も呼び出す可能性があります。一方、新しいCLIはログをバッチ処理し、最後に単一のconsole.logを実行します。これは場合によっては驚くほど速くなることもあります。

この領域での改善の主な余地は、視覚的に興味深い何かを行うことで、ユーザーがそれを見て忙しくしている間に、実際のパフォーマンスよりも知覚されるパフォーマンスがさらに重要になるためです。しかし、それを実行する際には、コンピューターをほとんど忙しくさせない方法で行う必要があります。

結果

まとめる前に、Babelのモノレポでファイルをチェックした際の、いくつかの数字を紹介します。すべてのファイルがフォーマットされている状態で、9つのエラーファイルがあり、さまざまなフラグを使用し、現在のCLIと新しいCLIでの結果です:

prettier packages --check # 29秒
prettier packages --check --cache # 20秒

prettier@next packages --check --no-cache # 7.3秒
prettier@next packages --check --no-cache --parallel # 5.5秒
prettier@next packages --check # 1.3秒

デフォルトでは、同じコマンドの時間が約29秒から約1.3秒に短縮され、約22倍の速度向上があります。これには、実行間でキャッシュファイルを記憶する必要があります。将来的には、50倍の速度向上に近づけることができるかもしれません。

キャッシュファイルが記憶されない場合、または明示的にキャッシングをオフにする場合、またはこれが初回実行の場合、並列化を使用して約29秒から約5.5秒に短縮されます。これは私のコンピュータで約5倍の速度向上で、かなり顕著なものでしょう。

Prettierのformat関数自体を変更せずに、この改善が達成されたことを再度指摘する価値があります。

Biomeとの比較

Biome、先進的なRustフォーマッターであり、おそらくパフォーマンスチャンピオンであると思われるものと、我々が得た数値を比較するのは興味深いことです:

biome format packages
# 診断は表示されません:25938。
# 28703ファイルを869ミリ秒で比較
# 4770ファイルをスキップ

ここでBiomeは、.gitignore.prettierignoreの解決をまだ実装していないようで、我々のCLIと比較して約11kも多いファイルのフォーマットをチェックしています。他にも、おそらく重要な、振る舞いの違いがあるかもしれません。

無視ファイルのサポートを無効にして、Biomeの振る舞いをより密接に模倣しようとすると、以下の数字が得られます:

prettier@next packages --check --no-cache --parallel # 15秒

2つのツールが正確に同じことを行っていないため、比較には少し注意が必要ですが、Biomeが多くのファイルのフォーマットチェックをどの程度の速度で行うかを見るのは興味深いです。これにはおそらくキャッシュファイルが必要でしょう。

ユーザーにとって大幅な速度向上を実現しようとする異なるアプローチです。

まとめ

新しいCLIはまだ進行中の作業ですが、ぜひ試してみてください!もうそれをインストールすることができます。

新しいCLIがもたらす速度向上を見てみたいと思います。結果を@PrettierCode@fabiospampinatoに直接ツイートしてください。特に、さらに速くするための質問やアイデアがあれば。

Discussion