Prettier 2.7 にキャッシュを実装した
Prettier 2.7 がリリースされました。
このバージョンには TypeScript 4.7 の対応のほかに、新しい CLI オプションである --cache
と --cache-strategy
が含まれています。
--cache
と --cache-strategy
を実装したのは自分なので、その背景や実装、そして使い方の話を雑にしようと思います。
背景
Rome Formatter のブログが公開されて日本の開発者からもそれなりに大きな反響がありました。
私個人としてはコードフォーマッターにそこまでの速さを求めていないのであんまり興味はなかった(もちろん速いほうがいいけど)のですが、みなさん意外と興味あるんだなあという気持ちで眺めていました。
それからしばらくして Prettier の https://github.com/prettier/prettier/issues/5853 という Issue が動いているのを見つけました。
その Issue で行われていたやりとりは、簡単にいえば
ユーザー「キャッシュっぽい機能がほしい」
メンテナー「PRくれれば見ます」
ユーザー「普通に Prettier 詳しくないから無理っす」
というようなものです。たしかに Prettier を初めて触る人がいきなり大きめの機能を実装するのは厳しいだろうと思います。
自分がその Issue を見つけたのはちょうどゴールデンウィークの最中で「まあ、暇だし、割と楽そうだし、みんな速いフォーマッター好きらしいから、やるかあ」という感じで着手しました。
暇だったし楽そうだったからという安直な理由で実装しましたが、Prettier を速くする方法としてキャッシュは適していると思います。Prettier はもともと速さを重視した設計ではないので、単純なフォーマットの速度を向上させるのは簡単ではありません。
そこで飛び道具的な方法としてキャッシュは適していました。結果として今回の --cache
は、既存の設計にほとんど影響を与えることなく大幅に速度を改善させることができました。
実装
PR は https://github.com/prettier/prettier/pull/12800 です。
実装はESLint の --cache
をほとんど丸パクリしました。
Prettier のキャッシュは、次のいくつかの要素をキャッシュキーとして扱います。
- Prettier のバージョン
- Prettier のオプション
- Node.js のバージョン
- (
--cache-strategy
がcontent
の場合)ファイルの内容 - (
--cache-strategy
がmetadata
の場合)ファイルのメタデータ(タイムスタンプとか)
これらのうちいずれかが直前のフォーマットから変更されていた場合に限り再度フォーマットを実行します。
ファイルの内容やメタデータの変更を検知するのには https://github.com/royriojas/file-entry-cache というライブラリを使っています。
実装は https://github.com/prettier/prettier/blob/5530ad24b952e0c48d6b13a17fbba0c45b645f8d/src/cli/format-results-cache.js らへんにあるので気になる人はそっちを見てください。
ちなみにキャッシュの保存場所は ./node_modules/.cache/prettier/.prettier-cache
です。これは nyc と AVA の間で決められたキャッシュ保存場所の標準に従ったものです(https://github.com/avajs/find-cache-dir を使っています)。
ちなみに ESLint には --cache-location
というオプションがあって、キャッシュファイルを置く場所を指定できます。Prettier でも当初それを実装していたんですが「(新しいオプションを追加してメンテするのは)めんどいから一旦なし!」ということで取り下げました。が、必要っぽい気もしてきたので追加で実装するかも...
使い方
基本的には --cache
ってつけるだけです。
prettier --cache --write .
みたいな感じです。
--cache-strategy
がやや難しいので解説します。
--cache-strategy
は --cache
と一緒に使うオプションで、名前のとおりキャッシュ戦略を変えることができます。--cache-strategy
は metadata
もしくは content
という値をとります。
--cache-strategy
が content
のときは、ファイルの内容自体がキャッシュのキーになります。つまりファイルの内容が変更されるとフォーマット対象になります。
--cache-strategy
が metadata
のときは、ファイルのタイムスタンプがキャッシュのキーになります。ファイルの内容が変更されたかどうかに関わらずタイムスタンプが更新されていればフォーマット対象になります。
この2つの --cache-strategy
にはトレードオフがあります。
まず metadata
については、タイムスタンプは Git に乗らないので CI 上では上手く機能しません。しかし content
に比べてキャッシュキーが更新されたかどうかを判定する際のパフォーマンスが良いです。
一方で content
はファイルの中身をキャッシュキーにするのでキャッシュファイルの入った ./node_modules
自体を上手くキャッシュしてやれば CI 上でも動作します(多分)。しかしファイルの中身が更新されたかどうかを確認するのはメタデータの更新をチェックするのに比べてパフォーマンス面では劣ります。
metadata |
content |
|
---|---|---|
for CI | x | o |
performance | o | x |
で、悩んだ結果デフォルトでは content
を使うようにしました。content
でも充分に速いので、利便性のメリットがパフォーマンスのデメリットを打ち消すと考えたからです。
おわりに
実装しやすい割に効果のあるの良い機能だと今の所は思ってます。
バグってる可能性は全然あるので、なにか見つけた方は報告してくれると助かります。
あともしこれを企業のえらい人が見ていたら、ぜひ https://opencollective.com/prettier へ寄付をお願いします。このままだと資金が底を尽きるので困ります。
Discussion