Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Flutterビルドキャッシュの内部構造とテスト高速化への応用

Avatar for Yoko Yoko
November 17, 2025
73

 Flutterビルドキャッシュの内部構造とテスト高速化への応用

FlutterKaigi 2025での登壇資料: https://2025.flutterkaigi.jp/

Avatar for Yoko

Yoko

November 17, 2025
Tweet

Transcript

  1. 自己紹介 経歴 1. LINEヤフー株式会社 2. 株式会社サイバーエージェント 3. トヨタ自動車株式会社 (NOW!) 趣味

    温泉、雑談、親切 職務 ソフトウェア改善チームのリーダー SRE的なお仕事 (指標設計、メトリクス計測、パフォーマンスチューニング、現場改善 etc) 横井 一樹 Kazuki Yokoi
  2. タイムテーブル 本セッションの全体時間は13:30 - 14:00 • ①13:30 - 13:40 ◦ 前振りなど

    ◦ ビルドフローの全体像把握〜関連用語・関係性について • ②13:40 - 13:50 ◦ サンプルAppのflutter runをもとに、ビルド成果物やキャッシュ 内部構造の仕組みについて • ③13:50 - 14:00 ◦ サンプルAppのflutter testをもとに、テスト高速化の手法 とそ の成果について
  3. `flutter run` (debugビルド) するとどうなるか ①`flutter run` bin/flutter → flutter_tools ②RunCommandで設定解決

    デバイス検出 , ターゲット, モード ③コンパイル制御 ResidentCompilerが frontend_serverを管理 ④frontend_server起動(常駐) dartコンパイラ/ランタイム ⑤Kernel(.dill)生成 フル or 増分 キャッシュ利用 ⑦端末へインストール /起動 以後の差分はDevFS + VM Serviceで転送 ⑥Assets/生成物バンドル Assets, fonts, l10n ・プログラムをロード ・プラグインを登録 ・main()実行  など ⑧Appプロセス開始 Engine + Dart VM起動 ⑨VM Serviceに接続 Log, Hot Reload制御 ⑩VM Service準備完了 Flutter SDK (flutter_tools) VM Service URI通知 アタッチ ProcessManager.start Kernel(.dill) 出力 データ転送&実行 Dart SDK (frontend_server / Kernelコンパイラ / CFE) 実機/エミュレータ (Flutter Engine / Dart VM)
  4. `flutter run` (debugビルド) するとどうなるか ①`flutter run` bin/flutter → flutter_tools ②RunCommandで設定解決

    デバイス検出 , ターゲット, モード ③コンパイル制御 ResidentCompilerが frontend_serverを管理 ④frontend_server起動(常駐) dartコンパイラ/ランタイム ⑤Kernel(.dill)生成 フル or 増分 キャッシュ利用 ⑦端末へインストール /起動 以後の差分はDevFS + VM Serviceで転送 ⑥Assets/生成物バンドル Assets, fonts, l10n ・プログラムをロード ・プラグインを登録 ・main()実行  など ⑧Appプロセス開始 Engine + Dart VM起動 ⑨VM Serviceに接続 Log, Hot Reload制御 ⑩VM Service準備完了 Flutter SDK (flutter_tools) VM Service URI通知 アタッチ ProcessManager.start Kernel(.dill) 出力 データ転送&実行 実機/エミュレータ (Flutter Engine / Dart VM) Dart SDK (frontend_server / Kernelコンパイラ / CFE) ビルドキャッシュの仕組みは Dart SDKに深く関係する 本セッションではここから深掘る
  5. 【再喝】`flutter run` (debugビルド) するとどうなるか ①`flutter run` bin/flutter → flutter_tools ②RunCommandで設定解決

    デバイス検出 , ターゲット, モード ③コンパイル制御 ResidentCompilerが frontend_serverを管理 ④frontend_server起動(常駐) dartコンパイラ/ランタイム ⑤Kernel(.dill)生成 フル or 増分 キャッシュ利用 ⑦端末へインストール /起動 以後の差分はDevFS + VM Serviceで転送 ⑥Assets/生成物バンドル Assets, fonts, l10n ・プログラムをロード ・プラグインを登録 ・main()実行  など ⑧Appプロセス開始 Engine + Dart VM起動 ⑨VM Serviceに接続 Log, Hot Reload制御 ⑩VM Service準備完了 Flutter SDK (flutter_tools) VM Service URI通知 アタッチ ProcessManager.start Kernel(.dill) 出力 データ転送&実行 実機/エミュレータ (Flutter Engine / Dart VM) Dart SDK (frontend_server / Kernelコンパイラ / CFE)
  6. Dart SDK内部の主要モジュール : 用語説明 frontend_server Kernelコンパイラ(CFE)を常駐プロセスとしてラップしたもの https://chromium.googlesource.com/external/github.com/flutter/engine/%2B/refs/heads/flutter-3.7-candidate.25/flutter_frontend_server/README.md Kernelコンパイラ DartをKernelへ変換する Kernel

    Dartの中間表現 https://github.com/dart-lang/sdk/tree/main/pkg/kernel .dill (dill = Dart Intermediate Language) Kernelのバイナリ表現を.dillという拡張子で保持する ④frontend_server起動(常駐) dartコンパイラ/ランタイム ⑤Kernel(.dill)生成 フル or 増分 キャッシュ利用 Dart SDK (frontend_server / Kernelコンパイラ / CFE)
  7. Dart SDK内部の主要モジュール : 用語説明 CFE (Common Front End) DartをKernelに変換するなど、FE処理全般を担う基盤ライブラリ群 https://github.com/dart-lang/sdk/tree/main/pkg/front_end

    ④frontend_server起動(常駐) dartコンパイラ/ランタイム ⑤Kernel(.dill)生成 フル or 増分 キャッシュ利用 Dart SDK (frontend_server / Kernelコンパイラ / CFE)
  8. Dart SDK内部の主要モジュール : 関係性 frontend_server 常駐コンパイラサービス Kernelコンパイラ DartをKernelに変換するコンパイラ ex: IncrementalCompiler

    CFE Dartフロントエンド基盤 Kernel Dartの中間表現 Dart上のClassのインスタンス群 インメモリで保持 .dill Kernelをバイナリ表現 オンディスクで保持 シリアライズ flutter_tools flutter_tools Dart SDK
  9. Dart SDK内部の主要モジュール : 関係性 frontend_server 常駐コンパイラサービス Kernelコンパイラ DartをKernelに変換するコンパイラ ex: IncrementalCompiler

    CFE Dartフロントエンド基盤 Kernel Dartの中間表現 Dart上のClassのインスタンス群 インメモリで保持 .dill Kernelをバイナリ表現 オンディスクで保持 シリアライズ flutter_tools flutter_tools Dart→Kernel(.dill) に変換する Dart SDK
  10. まとめ 以下をざっくり把握 • 主要モジュールの用語説明 ◦ frontend_server, Kernelコンパイラ, .dill, CFE •

    それらの関係性 • Dart SDKの役割の一つ ◦ Dart → Kernel(.dill) に変換する ④frontend_server起動(常駐) dartコンパイラ/ランタイム ⑤Kernel(.dill)生成 フル or 増分 キャッシュ利用 Dart SDK (frontend_server / Kernelコンパイラ / CFE)
  11. まとめ 以下をざっくり把握 • 主要モジュールの用語説明 ◦ frontend_server, Kernelコンパイラ, .dill, CFE •

    それらの関係性 • Dart SDKの役割の一つ ◦ Dart → Kernel(.dill) に変換する →なぜDartをKernelに変換する? ④frontend_server起動(常駐) dartコンパイラ/ランタイム ⑤Kernel(.dill)生成 フル or 増分 キャッシュ利用 Dart SDK (frontend_server / Kernelコンパイラ / CFE)
  12. なぜKernelに変換する? ①クロスプラットフォーム対応のため 1つの共通的な中間表現( =Kernel)を出発点として、各 PF向けの変換・最適化が行われる また、KernelはDart上のインスタンスのため、ネイティブ /Webへ入力する際は.dillとして引数に渡す • ネイティブ(iOS, Android,

    Windows, Linux, etc) ◦ Debug: Kernel → kernel_blob.bin (= .dill) → VMがJIT実行 ◦ Release: Kernel → AOTスナップショット → ネイティブコードとして実行 ▪ 各OSのネイティブ成果物(Android: libapp.so, iOS: App.frameworkのように) • Web(Chrome, Safari, Edge, etc)
  13. なぜKernelに変換する? ①クロスプラットフォーム対応のため 1つの共通的な中間表現( =Kernel)を出発点として、各 PF向けの変換・最適化が行われる また、KernelはDart上のインスタンスのため、ネイティブ /Webへ入力する際は.dillとして引数に渡す • ネイティブ(iOS, Android,

    Windows, Linux, etc) ◦ Debug: Kernel → kernel_blob.bin (= .dill) → VMがJIT実行 ◦ Release: Kernel → AOTスナップショット → ネイティブコードとして実行 ▪ 各OSのネイティブ成果物(Android: libapp.so, iOS: App.frameworkのように) • Web(Chrome, Safari, Edge, etc) ◦ Debug: Kernel → dartdevc → JS ◦ Release: Kernel → dart2js → JS(最適化された配布用バンドル)
  14. なぜKernelに変換する? ①クロスプラットフォーム対応のため 1つの共通的な中間表現( =Kernel)を出発点として、各 PF向けの変換・最適化が行われる また、KernelはDart上のインスタンスのため、ネイティブ /Webへ入力する際は.dillとして引数に渡す • ネイティブ(iOS, Android,

    Windows, Linux, etc) ◦ Debug: Kernel → kernel_blob.bin (= .dill) → VMがJIT実行 ◦ Release: Kernel → AOTスナップショット → ネイティブコードとして実行 ▪ 各OSのネイティブ成果物(Android: libapp.so, iOS: App.frameworkのように) • Web(Chrome, Safari, Edge, etc) ◦ Debug: Kernel → dartdevc → JS ◦ Release: Kernel → dart2js → JS(最適化された配布用バンドル) Kernel Dartの中間表現 ネイティブ iOS, Android, Windows, Linux, etc Web Chrome, Safari, Edge, etc Dart Debug Release Debug Release クロスプラットフォーム出力を一本化
  15. なぜKernelに変換する? ②開発速度向上のため Hot Reloadによって、Appを再起動せずに修正を即時反映できる これは実行中のDart VMに対して、 ”増分の.dill” をVM Service経由で適用することで実現している 【補足】

    ”増分”という表現について Q: 「”増分の.dill” と言っているが、コード差分を示しているのであれば 減ることもあるので ”差分の.dill” と表現した方が良いのでは?」 A: 「ここでの”増分”とはコードが増えるという意味ではなく、前回結果を再利用し、必要な部分のみを段階 的に再計算する方式を指すコンパイラ分野の慣用語。コード量の増減は関係ない」 増分コンパイル / Incremental Compilation
  16. なぜ.dillを復元すると、ビルドが高速になるのか frontend_server 常駐コンパイラサービス flutter run Dart SDK flutter_tools ResidentCompiler 【割愛】

    様々な処理 Kernelコンパイラ DartをKernelに変換するコンパイラ ex: IncrementalCompiler CFE Dartフロントエンド基盤 Appのエントリポイントなどの 情報を引数として渡す DartをKernelに変換する
  17. なぜ.dillを復元すると、ビルドが高速になるのか frontend_server 常駐コンパイラサービス flutter run Dart SDK flutter_tools ResidentCompiler 【割愛】

    様々な処理 Kernelコンパイラ DartをKernelに変換するコンパイラ ex: IncrementalCompiler CFE Dartフロントエンド基盤 Appのエントリポイントなどの 情報を引数として渡す DartをKernelに変換する 🔥コード量が多ければ多いほど、コンパイル時間が長くなる 🔥
  18. なぜ.dillを復元すると、ビルドが高速になるのか frontend_server 常駐コンパイラサービス flutter run Dart SDK flutter_tools ResidentCompiler 【割愛】

    様々な処理 Kernelコンパイラ DartをKernelに変換するコンパイラ ex: IncrementalCompiler CFE Dartフロントエンド基盤 Appのエントリポイントなどの 情報を引数として渡す DartをKernelに変換する ⭐.dillがあれば復元可能→コンパイル対象が減る ⭐
  19. ① .dart_tool/ 主な役割: 中間生成物置き場、依存関係の情報管理 flutter_build/<hash>/app.dill 完成されたKernel 各PFへ入力値として渡されたり、実行物としてコピーされる 例)iOS(シミュレータ)の場合 .dart_tool/flutter_build/<hash>/app.dill ↓

    build/ios/Debug-iphonesimulator/App.framework/flutter_assets/kernel_blob.bin https://github.com/flutter/flutter/blob/master/packages/flutter_tools/lib/src/build_system/targets/ios.dart#L666 package_config.json dartのパッケージ解決情報 flutter pub getすると生成される ※flutter runの冒頭ステップでもpub getされる https://github.com/dart-lang/tools/tree/main/pkgs/package_config
  20. ② build/ 主な役割: 各プラットフォームの完成物置き場 <hash>.cache.dill.track.dill ビルドキャッシュの中核 ビルド時、Dart SDKのfrontend_server に--initialize-from-dillというオプションで渡される =増分コンパイルの起点

    ios/ 例ではiOSだが、プラットフォームごとに dir生成される kernel_blob.binの所在(Dart VMがJITで読み込むファイル) build/ios/Debug-iphonesimulator/App.framework/flutter_assets/kernel_blob.bin プラットフォームごとの dir内のflutter_assets/に存在する
  21. <hash>.cache.dill.track.dillはどう生成されるか? ①ファイルパスを生成するメソッド 例) 9aa8b8c74e6af46c4be658e24cd3b943 .cache.dill.track.dill ②outputを生成 dartDefinesと cacheFrontEndOptionsを 一つの文字列に結合(=ビルド情報) dartDefines

    = [ ‘CLIENT_ID=jupiter’, ‘FLAVOR=development’, ‘API_KEY=abc’] flutter run \ --dart-define=CLIENT_ID=jupiter \ --dart-define=FLAVOR=development \ --dart-define=API_KEY=abc ③outputをmd5でハッシュ化 output = 一つの文字列にしたビルド情報 →9aa8b8c74e6af46c4be658e24cd3b943
  22. <hash>.cache.dill.track.dillはどう生成されるか? ①ファイルパスを生成するメソッド 例) 9aa8b8c74e6af46c4be658e24cd3b943 .cache.dill.track.dill ②outputを生成 dartDefinesと cacheFrontEndOptionsを 一つの文字列に結合(=ビルド情報) dartDefines

    = [ ‘CLIENT_ID=jupiter’, ‘FLAVOR=development’, ‘API_KEY=abc’] flutter run \ --dart-define=CLIENT_ID=jupiter \ --dart-define=FLAVOR=development \ --dart-define=API_KEY=abc ③outputをmd5でハッシュ化 output = 一つの文字列にしたビルド情報 →9aa8b8c74e6af46c4be658e24cd3b943. ④オプションと結合 ‘${buildPrefix}cache.dill’ =”③のハッシュ ”.cache.dill track-widget-creationが有効であれば、 末尾に ”.track.dill”を付与
  23. <hash>.cache.dill.track.dillはどう生成されるか? まとめ --dart-defineの構成をそのままキーの材料にしているため、 keyの増減はもちろん、valueの中身が変わるとキャッシュが無効になる 例)keyの増減 例)valueの変化 flutter run \ --dart-define=CLIENT_ID=jupiter

    \ --dart-define=FLAVOR=development \ --dart-define=API_KEY=abc flutter run \ --dart-define=CLIENT_ID=jupiter \ --dart-define=FLAVOR=development flutter run \ --dart-define=CLIENT_ID=jupiter 無効 flutter run \ --dart-define=CLIENT_ID=jupiter \ --dart-define=FLAVOR=development \ --dart-define=API_KEY=abc flutter run \ --dart-define=CLIENT_ID=neptune \ --dart-define=FLAVOR=development \ --dart-define=API_KEY=abc flutter run \ --dart-define=CLIENT_ID=neptune \ --dart-define=FLAVOR=production \ --dart-define=API_KEY=def 無効 無効 無効
  24. 補足 --dart-defineにはユーザ指定のものだけではなく、 SDK指定のものもある 実際に出力すると以下の通り 1. CLIENT_ID=jupiter 2. FLAVOR=development 3. API_KEY=abc

    4. FLUTTER_VERSION=3.35.1 5. FLUTTER_CHANNEL=stable 6. FLUTTER_GIT_URL=https://github.com/flutter/flutter.git 7. FLUTTER_FRAMEWORK_REVISION=20f8274939 8. FLUTTER_ENGINE_REVISION=1e9a811bf8 9. FLUTTER_DART_VERSION=3.9.0 Flutter SDK情報も含んでいるためSDK更新後には無効となる <hash>.cache.dill.track.dillはどう生成されるか?
  25. Dart SDKにdump_kernel.dartというスクリプトがある https://github.com/dart-lang/sdk/blob/main/pkg/vm/bin/dump_kernel.dart .dillを渡すと、人間が読むことが出来るレベルの .txtで出力される dart \ --packages=/Users/yokoi/dart/sdk/.dart_tool/package_config.json \ /Users/yokoi/dart/sdk/pkg/vm/bin/dump_kernel.dart

    \ build/9aa8b8c74e6af46c4be658e24cd3b943.cache.dill.track.dill \ output.txt dartコマンドでdump_kernel.dartを実行し、 第一引数に対象の.dillを、第二引数にアウトプットのパスを指定 ※Flutter SDKのbinには含まれていないので、試すには dart-langの環境を別途準備する必要あり .dillの中身を見る
  26. まとめ 見れるもの • 構造: Library → Class → Member(Field/Procedure) →

    Statement/Expressionのツリー • 意味情報: 型・定数(#C…テーブル)・null安全・メソッドシグネチャなど • 参照: canonical nameやIDで他ノードを指す 見れないもの • 元コードのフォーマットやコメント行は保持されない .dillの中身を見る
  27. 【まとめ】「ビルドキャッシュの内部構造」編 1. flutter runを実行してから、端末で起動するまでの全体像 2. 実行過程における、Dart SDK内部の主要モジュールの役割・関係性 ◦ frontend_server、Kernelコンパイラ、CFE、.dill 3.

    なぜDartをKernelに変換する必要があるのか ◦ 「①クロスプラットフォーム対応のため」と「②開発速度向上のため」 4. ビルドキャッシュの実体は.dillであること 5. なぜ.dillを復元すると、ビルドが高速になるのか 6. サンプルAppを例にした、ビルド成果物の主要ファイル ◦ /.dart_tool と /build 7. <hash>.cache.dill.track.dillはどう生成されるか ◦ ビルド情報に基づいている(dart-defineの構成など) ▪ なぜ基づく必要があるのか 8. .dillの中身を.txtに変換して実際に見ること
  28. 【まとめ】「ビルドキャッシュの内部構造」編 1. flutter runを実行してから、端末で起動するまでの全体像 2. 実行過程における、Dart SDK内部の主要モジュールの役割・関係性 ◦ frontend_server、Kernelコンパイラ、CFE、.dill 3.

    なぜDartをKernelに変換する必要があるのか ◦ 「①多ターゲット対応のため」と「②開発速度向上のため」 4. ビルドキャッシュの肝となるファイルは .dillであること 5. なぜ.dillを復元すると、ビルドが高速になるのか 6. サンプルAppを例にした、ビルド成果物の主要ファイル ◦ /build、/.dart_tool 7. <hash>.cache.dill.track.dillはどう生成されるか ◦ ビルド情報に基づいている(dart-definesの構成など) ▪ なぜ基づく必要があるのか 8. .dillの中身を.txtに変換して実際に見ること 以上を踏まえて、 「テスト高速化への応用」編に進みます 🚀
  29. `flutter test` するとどうなるか ①`flutter test` bin/flutter → flutter_tools ②RunCommandで設定解決 ターゲット,

    オプション ③コンパイル制御 TestCompiler, ResidentCompiler がfrontend_serverを管理 ④frontend_server起動(常駐) dartコンパイラ/ランタイム ⑤Kernel(.dill)生成 フル or 増分 キャッシュ利用 ⑥flutter_tester起動 ⑦.dillをロードして JIT実行 Engine + Dart VM起動 ⑧VM Service(任意) Flutter SDK (flutter_tools) ProcessManager.start Kernel(.dill) 出力 実行 Dart SDK (frontend_server / Kernelコンパイラ / CFE) flutter_tester (Flutter Engine / Dart VM)
  30. `flutter test` するとどうなるか ①`flutter run` bin/flutter → flutter_tools ②【割愛】 ③コンパイル制御

    TestCompiler, ResidentCompiler がfrontend_serverを管理 ④frontend_server起動(常駐) dartコンパイラ/ランタイム ⑤Kernel(.dill)生成 フル or 増分 キャッシュ利用 ⑥flutter_tester起動 Assets, fonts, l10n ⑦.dillをロードして JIT実行 Engine + Dart VM起動 ⑧VM Service(任意) coverage etc Flutter SDK (flutter_tools) ProcessManager.start Kernel(.dill) 出力 実行 Dart SDK (frontend_server / Kernelコンパイラ / CFE) flutter_tester (Flutter Engine / Dart VM) `flutter run` と同様に `flutter test` も Dart → Kernelに変換するステップがある (細かな違いはあるが割愛)
  31. `flutter test` するとどうなるか ①`flutter run` bin/flutter → flutter_tools ②【割愛】 ③コンパイル制御

    TestCompiler, ResidentCompiler がfrontend_serverを管理 ④frontend_server起動(常駐) dartコンパイラ/ランタイム ⑤Kernel(.dill)生成 フル or 増分 キャッシュ利用 ⑥flutter_tester起動 Assets, fonts, l10n ⑦.dillをロードして JIT実行 Engine + Dart VM起動 ⑧VM Service(任意) coverage etc Flutter SDK (flutter_tools) ProcessManager.start Kernel(.dill) 出力 実行 Dart SDK (frontend_server / Kernelコンパイラ / CFE) flutter_tester (Flutter Engine / Dart VM) `flutter run` と同様に `flutter test` も Dart → Kernelに変換するステップがある (細かな違いはあるが割愛) つまり、 .dillのビルドキャッシュが存在し仕組みも同様にある
  32. 仕様 unit testにて、 表示されているUIが各CLIENT_IDごとで期待通りかを検証するテストコード 実行コマンド flutter test \ --dart-define=CLIENT_ID=jupiter \

    --dart-define=FLAVOR=development \ --dart-define=API_KEY=abc \ --update-goldens --coverage 環境変数のCLIENT_IDごとにUIが切り替わる → ex) jupiter, mars, saturn, neptune … サンプルAppを`flutter test`
  33. 実行後、ビルドキャッシュの .dillを確認する ファイルパスが異なるので注意 • `flutter run` ◦ build/<hash>.cache.dill.track.dill • `flutter

    test` ◦ build/test_cache/build/<hash>.cache.dill.track.dill --dart-defineを変更して計10回実行したため、 <hash>.cache.dill.track.dillが10個存在している この状態であれば`flutter run`と同様に、 2回目以降の`flutter test`はビルドキャッシュが機能するため 実行が速くなる
  34. --dart-defineではなくランタイム環境変数を使う シングルトンなClient設定値 `flutter test`時のみ、 CLIENT_IDをランタイム環境変数で使用 それ以外は、 CLIENT_IDを--dart-define(コンパイル時定数)で使用 CLIENT_IDごとに分岐 case ‘venus’:

    case ‘mercury’: case ‘neptune’: case ‘jupiter’: … 【補足】ランタイム環境変数だとコード最適化が弱くなる releaseビルド(=AOT)におけるコンパイル時定数での分岐は、 該当値以外は成果物から削除される 一方でランタイム環境変数での分岐は、 該当値以外も成果物に含まれてしまう つまり、コード量削減がされないため `flutter test`のみに制限している