🔖

Prettier 2.7 にキャッシュを実装した

2022/06/15に公開

https://prettier.io/blog/2022/06/14/2.7.0.html

Prettier 2.7 がリリースされました。

このバージョンには TypeScript 4.7 の対応のほかに、新しい CLI オプションである --cache--cache-strategy が含まれています。

--cache--cache-strategy を実装したのは自分なので、その背景や実装、そして使い方の話を雑にしようと思います。

背景

Rome Formatter のブログが公開されて日本の開発者からもそれなりに大きな反響がありました。

https://rome.tools/blog/2022/04/05/rome-formatter-release

私個人としてはコードフォーマッターにそこまでの速さを求めていないのであんまり興味はなかった(もちろん速いほうがいいけど)のですが、みなさん意外と興味あるんだなあという気持ちで眺めていました。

それからしばらくして Prettier の https://github.com/prettier/prettier/issues/5853 という Issue が動いているのを見つけました。

https://github.com/prettier/prettier/issues/5853

その Issue で行われていたやりとりは、簡単にいえば

ユーザー「キャッシュっぽい機能がほしい」
メンテナー「PRくれれば見ます」
ユーザー「普通に Prettier 詳しくないから無理っす」

というようなものです。たしかに Prettier を初めて触る人がいきなり大きめの機能を実装するのは厳しいだろうと思います。

自分がその Issue を見つけたのはちょうどゴールデンウィークの最中で「まあ、暇だし、割と楽そうだし、みんな速いフォーマッター好きらしいから、やるかあ」という感じで着手しました。

暇だったし楽そうだったからという安直な理由で実装しましたが、Prettier を速くする方法としてキャッシュは適していると思います。Prettier はもともと速さを重視した設計ではないので、単純なフォーマットの速度を向上させるのは簡単ではありません。

そこで飛び道具的な方法としてキャッシュは適していました。結果として今回の --cache は、既存の設計にほとんど影響を与えることなく大幅に速度を改善させることができました。

実装

PR は https://github.com/prettier/prettier/pull/12800 です。

実装はESLint の --cache をほとんど丸パクリしました。

Prettier のキャッシュは、次のいくつかの要素をキャッシュキーとして扱います。

  • Prettier のバージョン
  • Prettier のオプション
  • Node.js のバージョン
  • (--cache-strategycontent の場合)ファイルの内容
  • (--cache-strategymetadata の場合)ファイルのメタデータ(タイムスタンプとか)

これらのうちいずれかが直前のフォーマットから変更されていた場合に限り再度フォーマットを実行します。

ファイルの内容やメタデータの変更を検知するのには https://github.com/royriojas/file-entry-cache というライブラリを使っています。

https://github.com/royriojas/file-entry-cache

実装は https://github.com/prettier/prettier/blob/5530ad24b952e0c48d6b13a17fbba0c45b645f8d/src/cli/format-results-cache.js らへんにあるので気になる人はそっちを見てください。

https://github.com/prettier/prettier/blob/5530ad24b952e0c48d6b13a17fbba0c45b645f8d/src/cli/format-results-cache.js

ちなみにキャッシュの保存場所は ./node_modules/.cache/prettier/.prettier-cache です。これは nycAVA の間で決められたキャッシュ保存場所の標準に従ったものです(https://github.com/avajs/find-cache-dir を使っています)。

https://github.com/avajs/find-cache-dir

ちなみに ESLint には --cache-location というオプションがあって、キャッシュファイルを置く場所を指定できます。Prettier でも当初それを実装していたんですが「(新しいオプションを追加してメンテするのは)めんどいから一旦なし!」ということで取り下げました。が、必要っぽい気もしてきたので追加で実装するかも...

使い方

基本的には --cache ってつけるだけです。

prettier --cache --write .

みたいな感じです。

--cache-strategy がやや難しいので解説します。

--cache-strategy--cache と一緒に使うオプションで、名前のとおりキャッシュ戦略を変えることができます。--cache-strategymetadata もしくは content という値をとります。

--cache-strategycontent のときは、ファイルの内容自体がキャッシュのキーになります。つまりファイルの内容が変更されるとフォーマット対象になります。

--cache-strategymetadata のときは、ファイルのタイムスタンプがキャッシュのキーになります。ファイルの内容が変更されたかどうかに関わらずタイムスタンプが更新されていればフォーマット対象になります。

この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 へ寄付をお願いします。このままだと資金が底を尽きるので困ります。

https://opencollective.com/prettier

Discussion