ViewReloaderのウォッチャー生成をビューパス登録後まで遅延させる

rails/rails

ActionView::CacheExpiry::ViewReloader において、ビューパスが登録されていない状態でウォッチャーを構築しないよう修正されました。これにより、Spring のようなプリローダーを使った環境で、不要なファイルシステムスキャンが大幅に削減されます。

背景

#51308(「最初の updated? チェックまでビューウォッチャーの構築を遅延する」)の修正には、エッジケースが残っていました。@watchernil のまま維持されることを前提とした遅延ガードが、dirs_to_watch が空配列の状態で updated? が呼ばれた場合に破綻していました。

その具体的なシナリオは Spring のウォームキャッシュ時に発生します。Spring はフォーク前に Rails.application.reloaders を直接走査して reload! が必要かどうかを判断します。この時点では Reloader.check! をバイパスするため、多くのエンジンの prepend_view_path 呼び出し(ActiveSupport.on_load(:action_mailer) / :action_controller_base のコールバック内に存在する)がまだ実行されていません。その結果、空のウォッチャーが生成されて @watcher が非 nil に固定され、以後の prepend_view_path ごとに Dir[*globs] + File.mtime による全スキャンが走るという問題が生じていました。

PRが示す極端な例として、prepend_view_path が10回呼ばれる場合の動作を以下に示します。修正前は10回のフルリビルドが発生していたのに対し、修正後はゼロになります:

ステップ 修正前 修正後
起動早期の updated? なし(安価) なし
prepend_view_path ×10 10回リビルド 0回リビルド
最初のリクエストの updated? リビルドなし 1回ビルド

Shopify の Rails モノレポで計測したところ、小規模なワークロードでの所要時間は 5.16 秒から 3.66 秒へと 29% 削減されています。

技術的な変更

この修正は、ウォッチャーフックの登録タイミングの変更と、build_watcher への空チェック追加という2点で構成されています。

フックの登録場所の移動

ViewReloader のコンストラクタ内で行われていた file_system_resolver_hooks へのフック登録が、actionview/lib/action_view/railtie.rb の初期化処理へと移動されました。

変更前(cache_expiry.rb内):

def initialize(watcher:, &block)
  @mutex = Mutex.new
  @watcher_class = watcher
  @watched_dirs = nil
  @watcher = nil
  @previous_change = false

  ActionView::PathRegistry.file_system_resolver_hooks << method(:rebuild_watcher)
end

変更後(railtie.rb内):

unless enable_caching
  view_reloader = ActionView::CacheExpiry::ViewReloader.new(watcher: app.config.file_watcher)
  ActionView::PathRegistry.file_system_resolver_hooks << view_reloader.method(:rebuild_watcher)

  app.reloaders << view_reloader
  app.reloader.to_run do

rebuild_watcher のパブリック化と build_watcher への空チェック追加

これまでプライベートメソッドだった rebuild_watcher が、外部から method(:rebuild_watcher) で参照できるようパブリックに昇格されました。また、build_watcher 内に「ビューパスが空でかつウォッチャーが未構築の場合はスキップする」ガード節が追加されています。

def rebuild_watcher
  return unless @watcher
  build_watcher
end

# ...

def build_watcher
  @mutex.synchronize do
    new_dirs = dirs_to_watch

    # Skip the build entirely if there are no view paths to watch and we have not built a watcher yet.
    return if new_dirs.empty? && @watcher.nil?

    old_watcher = @watcher
    # ...
  end
end

さらに updated? の戻り値も @watcher.updated? から @watcher&.updated? に変更され、ウォッチャーが nil のままでも安全に false を返せるようになっています。

今回の変更に対応するテストとして actionview/test/template/cache_expiry_test.rb が新規追加されました。FileUpdateChecker の代わりに CountingWatcher という軽量なスタブクラスを用い、ウォッチャーが実際に initialize された回数を計測することで、生成タイミングを精密に検証できる設計になっています。

設計判断

フック登録をコンストラクタの外へ出すという方針が採用されました。

rebuild_watcherViewReloader のコンストラクタではなく Railtie の初期化時にフックへ登録することで、インスタンスの生成と「パス変更への反応」を切り離しています。ウォッチャーはパスが実際に登録されるまで構築されず、rebuild_watcher が呼ばれても @watchernil であれば即座に返るため、誤ったタイミングでの早期構築が構造的に防がれます。

また build_watcher 内の return if new_dirs.empty? && @watcher.nil? は、「ウォッチャーが一度でも構築された後なら空のパスセットでも再構築を許容する」という意図を持ちます。既に実体化したウォッチャーを空ディレクトリで再構築するケース(パスが削除されるなど)を考慮した、防御的なガードです。

updated? での @watcher&.updated? の採用は、「ウォッチャーが nil の場合は更新なし(false)とみなす」というセマンティクスを明示し、nil チェックをメソッド呼び出し側に散在させない設計です。

本修正は、#51308 が開けてしまった「空のウォッチャーが誤って固定される」穴を、フック登録タイミングの調整と最小限のガード節によって塞いだ変更です。既存の遅延構築の設計思想を継承しつつ、Spring のようなプリローダー環境でも正しく機能するよう補強されており、変更範囲を最小化しながら確実な修正を実現しています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
9f3c8a5c

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)、背景・技術詳細(各論)、設計判断・まとめ(結論)の3部構成が明確で、非常に分かりやすい構造になっています。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライト(`言語:ファイルパス`)とGitHubのPRリンク記法(`[#123](URL)`)が正しく使用されています。

対象読者への適合性 ✓ PASS

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

Railsの内部構造に関する専門用語が適切に使用されており、専門知識を持つエンジニアという対象読者に完全に適合しています。

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

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

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

Diff内容との照合 ✓ PASS

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

記事内で引用されているコードブロックは、提供されたDiff情報と完全に一致しており、変更点が正確に反映されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「ViewReloader」「ウォッチャー」「Railtie」などの技術用語が、文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

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

変更の背景、技術的な実装、設計上の意図に関する説明は、すべて技術的に正確で論理的です。

事実の突合 ✓ PASS

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

パフォーマンス改善の具体的な数値やリビルド回数の比較など、記事内のすべての主張がPR Descriptionで裏付けられており、ハルシネーションは見られません。

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

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

PR番号(#51308, #57269)、パフォーマンス改善の数値(5.16秒→3.66秒, 29%)など、すべての数値・固有名詞がPR情報と一致しており正確です。

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

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

記事のタイトル「ViewReloaderのウォッチャー生成をビューパス登録後まで遅延させる」は、PRのタイトルと内容を正確に要約しています。

外部知識の正確性 ✓ PASS

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

記事の内容はすべてPR情報(Description, Diff)に基づいており、PRに記載のない外部知識の追加はありません。

時間表現の正確性 ✓ PASS

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

Springの起動シーケンスにおける「まだ実行されていない」といった時間的な前後関係が、PR情報に沿って正確に記述されています。