ファイルシステム走査の最適化で大規模プロジェクトの性能を改善

tailwindlabs/tailwindcss

Tailwind CSS v4のOxideスキャナにおいて、ファイルシステム走査を最適化することで、大規模コードベースでの処理速度が最大4倍に向上しました。複数回のファイルシステム走査の排除、並列ウォーカーの導入、そしてmtime追跡の遅延により、特に開発モードでの再スキャン性能が大幅に改善されています。

背景

Tailwind CSSのOxideスキャナは、プロジェクト内のファイルを走査してクラス候補を抽出します。この処理は、scanner.scan()でクラス候補を取得し、scanner.filesで監視対象ファイル一覧を取得するという2段階で行われていました。

小規模なコードベースではmtime(ファイルの最終更新時刻)追跡により不要な再スキャンを回避できていましたが、数千ファイルを持つ大規模プロジェクトでは、ファイルシステムを走査してmtimeを確認するだけでも無視できないオーバーヘッドが発生していました。#19616では、ディレクトリチェックが2回実行されることで約2倍の速度低下が報告されています。

技術的な変更

本PRは3つの主要な最適化を実装しています。

1. 重複するファイルシステム走査の排除

変更前の動作:

let scanner = new Scanner({ sources })
let candidates = scanner.scan()  // 1回目のファイルシステム走査
let files = scanner.files        // 2回目のファイルシステム走査

scanner.scan()の後にscanner.filesにアクセスすると、ファイル一覧を取得するために再度ファイルシステム全体を走査していました。

変更後の動作:

最初のscan()呼び出しで収集したファイルパス情報を保持し、filesプロパティへのアクセス時に再利用するようになりました。これにより、462ファイルのコードベースでも1.43倍、5000ファイルのコードベースでは1.92倍の高速化を達成しています。

2. 並列ファイルシステムウォーカーの導入

crates/oxide/src/scanner/mod.rsに並列処理機能が追加されました。ただし、並列ウォーカーには20-50msの起動オーバーヘッドがあるため、すべてのケースで有効というわけではありません。

実装された戦略:

  • 初回スキャン: 同期ウォーカーを使用(小規模プロジェクトで高速)
  • 2回目以降のスキャン: 並列ウォーカーに切り替え(大規模プロジェクトの開発モードで高速)

この切り替えにより、5000ファイルのコードベースでの開発モード再スキャンが2.19倍高速化しました。@tailwindcss/cliでは変更されたファイルを直接再スキャンできますが、@tailwindcss/postcssでは変更検知のためにファイルシステム走査が必要なため、この最適化が特に効果的です。

3. mtime追跡の遅延初期化

変更後の動作フロー(開発モード):

  1. 初回スキャン: ファイルシステムを走査してファイルパスを収集。mtimeは追跡しない
  2. 2回目のスキャン: 並列ウォーカーでファイルシステムを走査し、この時点からmtime追跡を開始。すべてのファイルを再スキャン
  3. 3回目以降: mtimeが変更されたファイルのみを再スキャン

2回目のスキャンで全ファイルを再スキャンするトレードオフはありますが、初回ビルドの高速化を優先した設計判断です。本番ビルド(1回のみのスキャン)ではmtime追跡のオーバーヘッドが完全に排除されます。

4. 細かな最適化

Cursorの構造体サイズ削減:

crates/oxide/src/cursor.rsCursor構造体が最適化されました。

変更前:

pub struct Cursor<'a> {
    pub input: &'a [u8],
    pub pos: usize,
    pub at_start: bool,
    pub at_end: bool,
    pub prev: u8,
    pub curr: u8,
    pub next: u8,
}

変更後:

#[derive(Debug, Clone, Copy)]
pub struct Cursor<'a> {
    pub input: &'a [u8],
    pub pos: usize,
}

impl<'a> Cursor<'a> {
    #[inline(always)]
    pub fn curr(&self) -> u8 {
        self.input.get(self.pos).copied().unwrap_or(0x00)
    }
}

フィールド数を7から2に削減し、prevcurrnextはメソッド呼び出しで計算するように変更されました。Cursorはクラス候補抽出中に頻繁に使用されるため、構造体のサイズ削減がメモリアクセス効率の向上につながります。

不要なアロケーションの削減:

pre_process_input関数のシグネチャが変更され、所有権を持つVec<u8>を受け取るようになりました。

変更前:

fn pre_process_input(content: &[u8], extension: &str) -> Vec<u8> {
    let mut result = content.to_vec(); // 常にコピーが発生
    // ...
}

変更後:

fn pre_process_input(content: Vec<u8>, extension: &str) -> Vec<u8> {
    let mut result = content; // 92%のケースでコピー不要
    // ...
}

呼び出し側で既にVec<u8>を持っている場合(全体の約92%)、.to_vec()による不要なメモリコピーが排除されます。

ソースパターンの事前正規化:

resolve_globs関数で使用されるソースパターンが、ウォーカーのフィルタ呼び出しごとではなく、事前に正規化されるようになりました。これにより、ファイルシステム走査中の繰り返し計算が削減されます。

設計判断

本PRでは、シンプルさと性能のバランスを重視した設計判断が複数行われています。

段階的な並列化の採用: 初回スキャンは同期、2回目以降は並列という切り替え方式により、小規模プロジェクトでのオーバーヘッドを避けつつ、大規模プロジェクトでの性能向上を実現しています。将来的にはコードベースサイズのヒント機能の追加も検討されています。

2回目スキャンでの全ファイル再スキャン: mtime追跡を遅延初期化することで初回ビルドを高速化する代わりに、開発モードの2回目スキャンでは変更の有無に関わらず全ファイルを再スキャンします。開発モードでのみ発生し、3回目以降は最適化されるため、許容可能なトレードオフと判断されています。

構造体サイズとメソッド呼び出しのトレードオフ: Cursor構造体のフィールドを削減し、メソッド経由で値を計算する方式に変更しました。メソッド呼び出しのコストよりも、構造体サイズ削減によるメモリアクセス効率の向上が優先されています。#[inline(always)]属性により、リリースビルドではメソッド呼び出しのオーバーヘッドが排除されます。

ベンチマーク結果

tailwindcss.comコードベース(462ファイル、13,200クラス候補):

  • 初回ビルド: 1.12倍高速化
  • 初回ビルド + .filesアクセス: 1.43倍高速化
  • 開発モード再スキャン + .filesアクセス: 1.19倍高速化

合成5000ファイルコードベース(5,000ファイル、5,005クラス候補):

  • 初回ビルド: 1.15倍高速化
  • 初回ビルド + .filesアクセス: 1.92倍高速化
  • 開発モード再スキャン: 2.19倍高速化
  • 開発モード再スキャン + .filesアクセス: 4.03倍高速化

大規模コードベースほど効果が顕著で、特に開発モードでの.filesアクセスを含むケースで最大の性能向上が確認されています。

まとめ

本PRは、ファイルシステム走査の重複排除、並列処理の段階的導入、mtime追跡の遅延初期化という3つの主要な最適化により、Oxideスキャナの性能を大幅に改善しました。構造体サイズの削減やアロケーションの最適化といった細かな改善も積み重ねられています。大規模プロジェクトの開発モードで最大4倍の高速化を達成しながら、既存のパブリックAPIを変更せず後方互換性を維持した実装となっています。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)、背景・技術詳細・設計判断(各論)、まとめ(結論)という「総論→各論→結論」の構成が明確で、ガイドラインに完全に準拠しています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```rust:crates/oxide/src/cursor.rs)やGitHubのPR/Issueリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

内容はファイルシステム走査、並列処理、mtime追跡、Rustの構造体最適化など専門的なトピックを扱っており、対象読者であるエンジニアに適した技術レベルです。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構成になっており、各段落はトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られています。可読性が非常に高いです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内のコードブロックは、Diff情報やPRの記述と正確に整合しています。特に`Cursor`構造体の変更や`pre_process_input`関数の変更意図を、Diff内容に基づいて的確に解説できています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「Oxideスキャナ」「mtime」「アロケーション」「インライン展開」など、使用されている技術用語は文脈に即しており、正確です。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

ファイルシステム走査の重複排除、並列化の導入、mtime追跡の遅延といった技術的な変更点に関する説明は、PRの記述と一致しており、技術的に正確かつ論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内の主張はすべてPRのDescriptionやベンチマーク結果で裏付けられています。例えば「最大4倍の高速化」はPRの「4.03x faster」という記述と一致しており、ハルシネーションは見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#19632)、Issue番号(#19616)、ベンチマークの数値(462ファイル、5000ファイル、高速化倍率など)はすべてPR情報と一致しており、正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトル「ファイルシステム走査の最適化で大規模プロジェクトの性能を改善」は、PRのタイトル「Improve performance in bigger projects」の内容を的確に反映しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PRの文脈から自明な「Tailwind CSS v4」以外の外部知識の追加はなく、捏造された情報はありません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「将来的には...検討」といった時間表現は、PR内の「in the future」という記述と一致しており、正確です。