Techtouch Developers Blog

テックタッチ株式会社の開発チームによるテックブログです

Biome と Git/GitHub を活用したJSバンドル差分表示ツールの開発

こんにちは!フロントエンドエンジニアの all-user です。
最近、家族全員で夜9時以降を緩めのデジタルデトックス時間にしてみました。
やってみるとオセロとかトランプみたいなアナログな遊びを模索するようになるのが面白いです。

さて今回は、Biome と Git/GitHub を活用したJSバンドル差分表示ツールについてご紹介します。

フロントエンド開発者が抱える漠然とした不安

こんな経験はありませんか?

「バンドラーに不要に見える設定があるけど消して良いか分からない...」
「babel 周りの設定を変更したけど意図したとおりの変更になってるよね?」
「モノレポのインターナルパッケージの解決方法を tsconfig の paths からパッケージマネージャーの workspaces 機能に変えたけどプロダクションコードは何も触ってないからビルド結果は何も変わらないはず」
「devDependencies のパッケージをアップデートしたけど、CI でしか使われてないパッケージだしプロダクションコードへの影響はないよね」

技術スタックの更新やレガシー設定の整理を行う際、CI は通るけど最終的なJSバンドルにどんな影響があったのか詳細は分からない...

こういった「漠然とした不安」を抱えながら作業を進めた経験が一度はあるのではないでしょうか。
弊フロントエンドチームでも、モノレポの技術スタックを段階的にアップデートしている中で同様の不安を抱えていました。
この不安を解消するために開発したのが、Biome と Git/GitHub を活用したJSバンドル差分表示ツールです。

開発は、Claude Code(Anthropic社が提供するAI開発支援ツール)を使用して、いわゆる「仕様駆動開発」のフローで進めました。
私は GitHub Actions のワークフローを一から実装した経験はほとんどありませんでしたが、期待通り動作するワークフローを組むことができました。
AI の力でアイデアをすぐに形にできるようになってきているのを感じます。

既存ソリューションの検討

まずはじめに、こういった課題に適した既存のソリューションがすでに存在していないかを調査しました。

ビルドの差分を比較・監視するツール・サービスはいくつか見つかったのですが、いずれも基本的にはバンドルサイズやモジュール構成の可視化にフォーカスしているものが多く、今回の要求に合う仕組みは見つけられませんでした。

差分表示ツールの設計

基本アイデア

出発点は「ビルド済みのJSをフォーマットしてから差分確認したい」というものです。
このアイデアの性質上、minify やフォーマットの過程で発生する差分こそ失われてしまいますが、それ以外の変化は基本的に全ての差分を検知できるはずです。

一番最初は、Biome でフォーマットをかけた後 unified diff を出力し、HTML で差分表示する仕組みでした。
そこから Claude Code との壁打ちを経て、GitHub のPR機能そのものを差分表示に利用する、というアプローチに途中で方針転換しました。
この転換により、以下のメリットが得られました。

  • 実装が簡潔になった
    • 差分表示に関する実装は不要になる
    • ビルドしてコミットしたブランチがビルド成果物のキャッシュとして機能する
  • 慣れ親しんだ GitHub ネイティブな差分比較体験を活用できる
    • レビュー機能やコメント機能も利用可能
  • Claude Code Actions などの AI によるレビューも組み込みやすい

※ パッケージマネージャーを yarn v4 に変更してみるテスト

※ AIレビューの様子

ブランチの運用方法

このツールでは、差分表示用の3つのブランチを作成して差分表示を実現しています。
この説明では、比較の基準となるブランチを①ベースブランチ、差分を確認したいブランチを②現在ブランチと呼んでいます。

差分表示用に作成されるブランチは以下の3つです。

  • ③ベースビルト*1ブランチ
  • ④現在ビルトブランチ
  • ⑤PR作成ブランチ

④現在ビルトブランチと⑤PR作成ブランチを区別する理由は、この2つを分けておくことで、比較基準となるベースブランチを変更しても④現在ビルトブランチの内容をキャッシュとして利用できるためです。

ワークフローの定義とconfigファイル(後述するビルド対象のプロジェクトやコマンドを指定するYAMLファイル)は②現在ブランチの物を使用します。

③ベースビルトブランチ

# ブランチ名例
feature/update-babel__build-diff/base_abc123d__conf_def456a
  • 比較基準となる①ベースブランチ(通常はgit merge-baseで取得した共通祖先コミット)でビルドした結果
  • ②現在ブランチのconfigファイルのハッシュも含むことで、設定変更時にキャッシュヒットしないようにしています

④現在ビルトブランチ

# ブランチ名例
feature/update-babel__build-diff/cur_xyz789g
  • ②現在ブランチでビルドした結果
  • tree hash を使うことで、同じコード状態での重複ビルドを回避します

⑤PR作成ブランチ

# ブランチ名例
feature/update-babel__build-diff/pr_xyz789g_abc123d
  • ④現在ビルトブランチにチェックアウト後、③ベースビルトブランチにソフトリセット
  • この結果、git history としては③ベースビルトブランチから派生、ファイル内容は④現在ビルトブランチと一致するブランチが作成されます

比較基準点の決定

デフォルトではgit merge-baseを使って比較基準を自動決定します。

git merge-base HEAD origin/main  # 共通祖先コミットを取得

git merge-base とは?

2つのブランチが分岐した最新の共通コミットを取得するGitコマンド。

これにより、

  • 純粋に②現在ブランチで行った変更のみが差分として表示されます
  • merge で取り込んだ main の変更は差分に含まれなくなります
  • origin/main が進んでも、共通祖先が変わらない限り比較対象が安定します

全体ワークフロー

実際の動作フローを図で表現すると以下のようになります。

PR作成ブランチ存在

PR作成ブランチなし

両方存在

一方または両方不在

GitHub Actions 手動実行

Setup Job

ブランチ名とハッシュ値の計算
・base_built_branch
・current_built_branch
・pr_creation_branch

既存ブランチ確認

全てスキップ
既存PRを利用

ビルトブランチ確認

ビルドをスキップ
PR作成のみ実行

並列ビルド実行

Base Build Job

Current Build Job

1. base_branchをチェックアウト
2. scripts/build-diff を current_branch から取得
3. ビルド実行(現在の設定でbaseをビルド)
4. Biome フォーマット
5. current_branch をチェックアウト
6. リポジトリクリーン
7. ビルド成果物を配置
8. base_built_branch 作成・push
1. current_branch で作業
2. ビルド実行
3. Biome フォーマット
4. current_branch をチェックアウト
5. リポジトリクリーン
6. ビルド成果物を配置
7. current_built_branch 作成・push

ビルド完了待機

Create PR Branch Job

1. current_built_branch をチェックアウト
2. pr_creation_branch を作成
3. base_built_branch に soft reset
 → historyはbase、内容はcurrent
4. CLAUDE.md を追加
5. コミット・push

Draft PR 作成

差分表示完了

導入の効果

レビュー品質の向上

「babel設定を変更」→「実際にこの部分のコードが変わりました」を具体的に示せるようになりました。

レガシー設定の削除が容易に

「この設定、本当に不要?」の判断がしやすくなり、安心してレガシー設定を削除できるようになりました。

予測と結果の一致確認

必ずしも分かりやすい差分が出るとは限りません。ビルドツールを大きく変更した場合、比較困難なビルド結果になることも当然あります。また、予想に反して全く差分が出なかったり、逆に想定よりも大きく差分が出ることもあります。こうした「肌感と実際の結果」を照合できることも重要な価値だと思いました。

JSファイル以外の成果物差分も可視化

Biome のフォーマット対象はJSファイルのみですが、指定したディレクトリをまるっとコミットするため、他のファイルについても差分を確認できるようになりました。ディレクトリ構造やファイル数、ファイル名の違いなども一目で分かるため、ビルド設定変更の影響を包括的に把握できます。

実装上の工夫

Biome による高速フォーマット

Biome の高速な処理速度によりフォーマットにかかる時間を、Prettier に比べて約1/7に抑えることができました。
Prettier との処理速度を比較してみます。

Biome設定

{
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineEnding": "lf"
  },
  "files": {
    "ignoreUnknown": false,
    "maxSize": 10485760
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "double"
    }
  }
}

Biome では、パフォーマンス上の理由からデフォルト値(1048576 = 1MB)を超えるファイルは無視されるため、maxSizeを 10MiB(10485760バイト)に設定しています。

参考: Biome 公式ドキュメント - files.maxSize

Prettier設定

{
  "semi": true,
  "trailingComma": "es5",
  "singleQuote": false,
  "tabWidth": 2,
  "useTabs": false,
  "endOfLine": "lf"
}

両設定とも、2スペースインデント・ダブルクォート・LF改行で統一して比較します。

対象ファイル: 3.9MB の minify されたJSファイル(dist/main.js

# Biome
yarn biome format --config-path=scripts/build-diff/biome.build-diff.json \\
  --max-diagnostics=0 dist/main.js --write
# → 実行時間: 1.72秒

# Prettier
yarn prettier --config scripts/build-diff/prettier.build-diff.json \\
  --write dist/main.js
# → 実行時間: 12.40秒
  • Biome: 3.9MB を1.72秒で処理(約7.2倍高速)
  • Prettier: 3.9MB を12.40秒で処理

完成した差分表示ツールの実測値では、ワークフロー全体で Biome によるフォーマット処理に約40秒かかっていたため、もし Prettier を使用した場合は単純計算で7倍の約280秒になっていたことになります。
ビルドやPR作成も含めたワークフロー全体の実行時間は現在12分前後で、フォーマット処理がワークフローの大部分を占めているわけではない(一部を並列化しているおかげもあり)ものの、Biome によりフォーマットを高速に処理できるようになったのは大きいと感じました。

Git の Tree Hash ベースのキャッシュ戦略

同じコード状態での重複ビルドを避けるため、Git tree hash を利用しています。

# tree hashを取得してブランチ名に組み込み
TREE_HASH=$(git rev-parse HEAD^{tree} | cut -c1-7)
BUILT_BRANCH="feature/update-babel__build-diff/cur_${TREE_HASH}"

# 既存ブランチをチェック
if git show-ref --verify --quiet refs/remotes/origin/${BUILT_BRANCH}; then
  echo "キャッシュされたビルド結果を使用"
fi

tree hash とは?

Git が管理するディレクトリツリーの状態を表すハッシュ値。同じファイル内容・構造なら同じハッシュになるため、今回のケースではキャッシュの判定に最適です。

マルチプロジェクト対応

YAML形式のconfigファイルで、複数プロジェクトに対してビルドコマンドやビルド成果物の出力先ディレクトリ・対象ファイルを指定できるようにしています。

projects:
  - name: player
    commands:
      - yarn build player
    includes:
      - dist/apps/player/**/*.{js,txt,html,json}
  - name: editor
    commands:
      - yarn build editor
    includes:
      - dist/apps/editor/**/*.{js,txt,html,json}

ブランチ間でビルドコマンドが異なるケースや対象プロジェクトが増減するケースに対応

ベースブランチと現在ブランチとの間で、ビルドコマンドや出力されるファイル名・ディレクトリ構造が異なるケースも想定されるため、それらに対応できるような設計にしました。

projects:
  - name: player
    commands:
      - yarn build player
    includes:
      - dist/apps/player/**/*.{js,txt,html,json}
    branch_mapping:
      commands:
        - npm run build-player
  • branch_mapping.commands でベースブランチ側のビルドコマンドをオーバーライドできるようにしています
  • 複数コマンドの逐次的な実行を指定できるように配列形式を採用
    • これを利用してファイル名・ディレクトリの違いを吸収するような処理(リネームや移動など)を行うことができます

また、対象プロジェクトが追加・削除されるケースにも対応するため、片方のブランチにしかプロジェクトが存在しない場合も正常ケースとして扱うようにハンドリングしています。

仕様駆動開発

本ツールの開発では、Claude Code を使用して次の段階を踏みながら開発を行いました。

  1. 仕様書作成: 要件と設計を詳細に文書化
  2. 設計レビュー: 方針が不明確な点や誤りがないかをチェックし、問題があれば1に戻りこの手順を繰り返す
  3. 実装着手: 設計が固まってから実装開始

仕様書作成も実装もアウトプットは基本的に Claude Code に任せていたため、自分が行なっていたのは9割以上設計・実装レビューという感じでした。

実装やワークフローを実際に実行してみてははじめて気づくポイントはどうしても出てきましたが、最初にしっかりと仕様を詰めておいたおかげで、実装フェーズでは Claude Code がほぼ自走できる状態でした。

また、作成した設計書はそのまま README.md として活用しています。

その他思ったことなど

ブランチめっちゃ生える

ツールの仕組み上、一回の差分比較のたびに新しいブランチが3つ作成されるため、ブランチ一覧が汚れてしまう問題があります。

今のところ差分を見たい時に手動で GitHub Actions を叩く運用にしているので、爆発的に増えたりはしていないのと、__build-diff という識別可能な文字列を含む命名にしているので、抽出して一括で削除することは可能ですが、ちょっと気になっている点ではあります。

対策として、PRを閉じた際に自動的にブランチも消えるようにする仕組みを検討中です。

git submodules を使う案

上記のブランチが増えてしまう話にも関連して、git submodules を使ってツールを外部リポジトリとして管理する設計も少しだけ検討しました。

こちらの方が構成としてはあるべき姿な気もしつつ、同じリポジトリであることのメリット(ブランチの参照などがしやすく、AIによる自動レビューの仕組みも流用しやすい)も感じていたため、新しくリポジトリを設定することや git submodules のキャッチアップを心理的に敬遠してしまい、あまり深くは検討できていません。

また再考する機会があれば、この構成も検討してみたいです。

まとめ

弊フロントエンドチームでは、技術スタックの更新時に「漠然とした不安」を感じていました。今回開発した差分表示の仕組みにより、Git/GitHub の既存機能を活用しながら、最終成果物であるJSバンドルの差分を効率的に可視化できるようになりました。

技術スタックの更新で不安を感じている開発者の皆さんの参考になれば幸いです。同様の課題を抱えている方は、ぜひこのアプローチを参考にしてみてください。

*1:ビルドに使用するブランチ(build)ではなく、ビルド済みの成果物をコミットするブランチ(built → ビルト)、という意図を表現したかったので「ビルト」と表記しています。