フロントエンドのlinterをESLintからBiomeへ移行したら開発体験がいい感じに向上した話
はじめに
こんにちは。ALGO ARTIS でソフトウェアエンジニアをしている宇藤 恭平です。プライベートでは子育ての傍ら、観葉植物を愛でたり、Rustを書いたりしています。
これは何の話
ALGO ARTISでは、社会基盤を支えている各企業様に対して、運用計画最適化ソリューションを提供しています(例: 火力発電所における燃料運用計画の最適化などが弊社HPにて公開されています)。
現在、最適化ソリューションは共通フレームワークの上に載せるものとして開発されているのですが、この共通フレームワークのフロントエンド部分のlinterを、ESLintからBiomeへ移行したときの話をします。
Biomeとは
聞いたことがある方も多いと思いますが、そもそもで「Biomeとは?」をまとめてみます。
一言でいうとBiomeは、「フロントエンドの開発ツールチェイン状況に革命を起こそうとしたプロジェクトの後継」です。
たとえばRustであれば、cargo(パッケージマネージャ)を筆頭に、フォーマッタ(cargoに内蔵)、clippy(linter)などが公式のプロジェクトとしてメンテナンスされているため、「この言語で書こう」となったときにlinterやformatterのチョイスで迷うことは皆無、と言ってよいでしょう。また、rustup(コンパイラや標準ライブラリのインストーラ)が存在するため、複雑な導入プロセスとも無縁です。
一方JavaScriptはlinterからformatter、テストツール、バンドラにいたるまで、種々のツールを自らチョイスして組み合わせ環境を構築する必要があります。
※ 実際にはviteに対するvitestなど、お互いに紐づいて使用される、もしくは推奨を提示されるツール群も存在していますが、あくまで任意、という位置付けと理解しています。
これがデメリットばかりかというとそういうわけでもなく、「何かのドメインにおいて特定のツールの使用を強制されない」というのは、見方によっては自由の担保にもなりえます。実際この自由さがJavaScriptを使った開発の面白みの一つでもあるように思うのですが、とはいえ組み合わせが増えるにつれてそれぞれに対する設定が必要になり、複雑性が高まってしまうことは否定できません。
この状況を解決しようと乗り出したのがRomeプロジェクトでした。すでにGitHubのリポジトリはアーカイブされていますが、そこにはこうあります。
平たくいうとRomeはバンドラ、linter、formatter、そしてテストツールを統合した一つの巨大な開発ツールを標榜していました。Romeをインストールさえすればこれら全てが賄える世界を目指していたのです。最初はTypeScriptで書かれていたようですが、その後Rustでの開発へ移行しています。ノリとしてはまさしくcargoのような存在ですね。
元々はMeta傘下のOSSとして出発したRomeはその後独立し、開発が続けられましたが、途中でその歩みは頓挫することになりました。その理由を、後継であるBiomeのコアチームメンバはあまりはっきりとは語っていませんが、おそらく「デカすぎるものを最初から作ろうとした」というのがその一因であろう、と自分としては感じています。開発者にとっては最初から巨大なものを志向すること自体にリスクがあり、一方でユーザーは実はそこまで大きいものを望んでいるわけでもなかった…というところに課題があったのではないでしょうか。
しかしRomeが夢見たものを引き継ぎたい、という人々がいました。彼らはRomeをフォークし、Biomeと名付けて再スタートを切りました。
Announcing Biomeを読んでみるとBiomeは依然、「すべてを統合した最強ツール」になることを諦めてはいないようです。が、現状、Biomeはlinterおよびformatterに特化し、それぞれ主にESLintとPrettierを代替するものとして、開発が続けられています。
※ 後述するようにanalyzerもBiomeに含まれていますが、これは現状import orderのsortのみを機能として提供しています。
この「選択と集中」への方針転換は、ユーザーにも比較的好意的に受け入れられているように見えます。
なぜ移行したのか
表題の通り、もともと共通フレームワークにおいてはESLintを使用していました。
ESLint自体に何か特別厄介な不具合があったとか、問題を抱えていたというわけではありません。ただ気になることとしてはやはり、「実行に時間がかかる」というものがありました。 当時のコミットに戻ってみると、素の状態で約8秒、ESLintの実行にかかっています。キャッシュは効かせられるものの、わずかな変更を加えただけでも2秒前後かかっており、毎回地味にストレスを感じていました。
また、この実行時間はCIの実行にもそのまま乗ってくるものでもあります。当時も今も、linterをGitHub Actionsのworkflowでまわしているのですが、GitHub Actionsの料金は実行時間とストレージ使用量に基づいています。linter部分の実行時間は全体の実行時間からすれば大きくないものではあるのですが、とはいえ短いに越したことはありません。
また体験としても、PRを立て、CIが走り終わるのを待つ時間は意外と長く感じることがあります。これらを、ESLintよりも速いlinterを採用することで多少とも改善できるのであれば、十分ペイする、との考えの元、Biomeへの移行を決めました。
…というのがかっこいい感じの理由です。
本音
上記は決して嘘ではないのですが、実際にはもう少し軽い気持ちで移行を始めていました。それは「linterは所詮linterなので、やってみてうまくいかなかったら戻せばいい」というもの、そして「新しいツールを使う機会は逃したくない」というものです。
linterはたとえばtscの型チェックと違い、「違反していても最悪動く」ものです。コーディングスタイルの問題でしかない、と言ってしまって差し支えないものも多分に含まれており、変更することにそこまで実際的なコストを感じないドメインではありました。だからこそ思い切って移行できた、という面はあると思います。
また、これは後述しますが、Biomeの提供している機能のうち、linterは使用していますが、formatterは実は採用せず、これまでどおりPrettierの使用を継続しています。これは、Prettierからのformatterの移行は相当大きな差分を出すから、というのが理由で、その意味では「差分が極力出ない移行をトライしてみた」というのが正確な表現になると思います。
また、「新しいものを触ってみたい」については、多くを語る必要はないでしょう。このような気持ちを共有できるチームで本当によかったです。
移行検討
「速いESLint代替の検討」の初期段階で、候補はBiomeほぼ一択でした。理由は大きく2つで、まず「速い」こと、そして「互換性」です。
速度
当時のベンチマークが残っていないので、移行PR前後のコミットに戻り、cacheを使用しない形で試してみます。
hyperfine "yarn eslint '**/*.{js,jsx,ts,tsx}' --quiet"
Benchmark 1: yarn eslint '**/*.{js,jsx,ts,tsx}' --quiet
Time (mean ± σ): 8.131 s ± 0.148 s [User: 12.152 s, System: 1.262 s]
Range (min … max): 7.911 s … 8.459 s 10 runs
hyperfine 'yarn biome check .'
Benchmark 1: yarn biome check .
Time (mean ± σ): 488.4 ms ± 57.6 ms [User: 1137.4 ms, System: 190.6 ms]
Range (min … max): 467.7 ms … 652.3 ms 10 runs
8.131s / 488.4ms = 16.64… ということでlinter自体のナイーブな実行速度は約16倍になっています。これは単純に気持ちがいいですね。
ESLint互換
また、BiomeがESLintとの互換性をかなり意識して開発されていることも、選定の大きな理由の1つでした。
と公式にある通り、おおまかには同じ感覚でlintルールを扱うことができます。
ただしBiomeの設定ファイルであるbiome.jsonに、ESLintの設定をそのままそっくりコピペするのは難しい部分もあります。これについては後述します。
移行のリアル
元々使用していたESLintの設定は非常にシンプルなものでした。pluginとrulesを抜き出すと以下になります。
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:jest/recommended",
"prettier",
"plugin:import/errors",
"plugin:import/typescript",
],
rules: {
"@typescript-eslint/ban-ts-comment": ["off"],
"@typescript-eslint/no-unused-vars": ["off"],
"@typescript-eslint/no-explicit-any": ["warn"],
"sort-imports": ["off"],
"no-unreachable": ["off"],
"import/order": [
"error",
{
alphabetize: { order: "asc", caseInsensitive: false },
"newlines-between": "always",
},
],
"import/newline-after-import": ["error"],
},
anyの使用もwarnに留めています。見る人によってはギョッとされるかもしれませんが、”スタートアップにおける開発とメンテナンスのバランス”ということでおさめていただければと思います。
ここからBiomeにどう移行するか、ですが、まずBiomeの機能群をおおまかに把握する必要があります。前述した通り、Biomeの現状はざっくりいうとlinterとformatterが組み合わさったものです。linter部分は採用するとして、formatterについて、元々使っていたformatter(Prettier)を使い続けるか、移行するかを決めなければなりませんでした。そして我々のとった選択は、「Biomeのformatter部分は採用しない」でした。
その主な理由としては、「Biomeのformatterはそれなりにopinionatedなものになっており、差分が非常に多く出ること」、そして「formatを強く強制するようなルールをチームとしては採用していないこと」、この2つでした。導入することにより、本質的でない理由でコードが大幅に書き換えられてしまうことは避けたかったですし、それを行うモチベーションもそれほどなかったというところです。
このあたりはformatterの扱いがそれぞれのチームでどうなっているか次第だと思います。きっちり運用していきたいのであれば、Biomeをformatter込みで運用するのもリーズナブルな選択肢だと個人的には感じます。
一方で、先に挙げた通りimport orderについては何かしらを差し込みたい気持ちもありました。これについては全く同じ設定を導入することは難しいのですが、Biomeもimport sortingの機能を提供してくれています。
https://biomejs.dev/analyzer/import-sorting/
linter, formatterに次ぐ第3の機能として設定されているanalyzerがそれで、現在はimport sortingのみの提供なので、これをそのまま使うことにします。
"organizeImports": {
"enabled": true,
"ignore": [
"**/node_modules/**",
...
また、formatterは採用しないのでこうなります。
"formatter": {
"enabled": false
},
linterの設定
さて、肝心のlinterの設定です。
考え方としては、「推奨ルールを入れた上で、挙動が変わらないよういくつかのルールを調整していく」となります。ちょうどESLintのeslint:recommendedを導入するような感覚ですね。
Biomeはrecommended rulesというのを提供してくれており、これが推奨ルールに当たります。
https://biomejs.dev/linter/rules/#recommended-rules
ちなみに、Biome公式で、コマンドラインからESLintの設定ファイルを読み込み、自動でBiomeの設定ファイルを吐き出してくれる動線も用意されているのですが…これは個人的にはおすすめしません。
https://biomejs.dev/guides/migrate-eslint-prettier/
このコマンドは必ずしもrecommended rulesをenabledにした上で各種調整をしてくれるものではなく、結果としてルールの記述がいたずらに増えてしまう可能性があります。また、推奨ルールがenabledでない場合、扱っているルールの網羅性が曖昧になり、「linterを使うことでどうコードを改善していけばいいのか」がわからなくなってしまいます。
推奨ルールをonにしておくと「明示的にoff、もしくはwarningにしているルールをonにしていけば推奨状態に近づき、結果としてコードの治安が改善されていく」ことが明確になります。これがlinterを使う最大のメリットでしょう。
というわけで、面倒でも手動でlinterの結果をas-isに近づけていくことをおすすめしたいと思います。この作業により、「こんなところでこんなlintエラーが出ている!」と、気づかなかった部分が目につくという嬉しい(?)副作用もあります。
完成したlinter設定の冒頭はこんな感じです。
"linter": {
"enabled": true,
"ignore": [
"**/node_modules/**",
...
],
"rules": {
"recommended": true,
"correctness": {
"useExhaustiveDependencies": "off",
"useJsxKeyInIterable": "off"
},
"suspicious": {
...
ちなみにBiomeでは、ruleはいくつかのカテゴリに大別されています(このリスト自体、ざっと眺めていくと学びがあり、なかなか面白いです)。
https://biomejs.dev/linter/rules/
結果的にどのカテゴリでruleが特にoffになったかを見てみると、コードの状況がなんとなく把握できるかもしれません。ちなみに弊社ではstyleカテゴリが最も多かったです。
recommended rulesは当時、全部で186個でしたが、最終的にoffにした・もしくはwarning扱いにしたruleの数は39個でした。これを多いと見るか少ないと見るかは人それぞれだと思いますが、ESLintのときよりも改善ポイントが非常にクリアーになった感覚を持っています。
CIのセットアップ
CIについては、これもBiome公式が一通りのサンプルを用意してくれています。
name: Code quality
on:
push:
pull_request:
jobs:
quality:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Biome
uses: biomejs/setup-biome@v2
with:
version: latest
- name: Run Biome
run: biome ci .
が、このままPRを立てたところ、社内のJSの達人から待ったがかかりました。何が問題か分かるでしょうか?
答えは、「この内容だとローカルとCIで使用するBiomeのバージョンが違うものになる可能性がある」です。uses: biomejs/setup-biome@v2のところで、実はBiomeをグローバルインストールしているので、「ローカルでは通るがCIでは落ちる」というような状況が発生する可能性が出てきます。
これを避けるためには、シンプルにローカルのBiomeを実行する形にすれば十分です。
- name: Run Biome
run: yarn biome ci .
振り返って & 仲間募集
移行してどうだったか、ですが、まずlinter起因でトラブルが起こるようなことは今のところ一切ありません。意図通り、きちんと動いています。
また(わかっていたことですが)linterのレスポンスが非常に速くなったことで、開発体験は確実に向上したと思っています。削減できたのはわずかな時間ですが、積み重なればそれなりのものになります。早く着手できればできるほど効果の大きいタイプの改善ですね。
加えて、設定自体が非常にシンプルなので、治安を改善しようと思えばルールを見て一つずつ潰していけばいい、というところもメンテナンス性の向上につながっています。
総じてやって損のない、非常におすすめの改善策ですので、linter周りで何か改善ができないかと検討中の方はぜひ、Biomeを試してみてください。
そしてALGO ARTISでは現在、一緒にフロントエンドからサーバーサイド、インフラまで、急成長中のプロダクトをゴリゴリ開発する仲間を募集しています。新しいものに貪欲で、アウトカムに対して誠実。そんなチームで一丸となって楽しく働いていますので、少しでも興味のある方はぜひ、カジュアル面談などでご連絡をいただけたら幸いです。
良かったら、SNSやHPをチェックしてみてください。最新情報をご覧いただけます。また、フォローやスキ♡もお待ちしています!(スキはNoteのアカウントがなくても可能です!)
ALGO ARTIS について:https://www.algo-artis.com/
最適化ソリューション『Optium』:https://www.algo-artis.com/service
化学業界DXソリューション『Planium』:https://planium.jp/
X : https://x.com/algo_artis
Linkedin : https://www.linkedin.com/company/algo-artis/