はじめに
こんにちは、STORESの @tomorrowkey です。STORESでは STORES ブランドアプリ の開発を行っています。 4月19日に開催された Ebisu.mobile #5〜モバイルアプリの品質改善どうしてる?〜 でお話しした「Compose Compiler Metricsを使った実践的なコードレビュー」についてブログにもまとめておきます。
ブランドアプリの状況
皆さんのプロダクトのAndroidアプリでも同様かと思いますが、STORES ブランドアプリでもJetpack Composeへの移行を進めています。せっかくアプリのフロントエンドを書き換えるのであれば、パフォーマンスも気にしたいですよね。では、Jetpack Composeでパフォーマンスが良い実装とはどのようなものでしょうか?
Jetpack Composeでパフォーマンスのよい実装をするには
Jetpack Composeでパフォーマンスのよい実装をするには、無駄な再描画を抑える、つまりできるだけRecompositionを抑えることがポイントとなります。 Recompositionを抑えるためには、Composable関数をできる限り Skippable にする必要があります。 Composable関数をSkippableにすることで、同じ入力値(パラメータ)であれば、出力値(表示)は同じになるので、Recomposition時にキャッシュを活用でき、パフォーマンスの向上が期待できます。
どのComposable関数がSkippableなのか
Composable関数をSkippableにするために重要になるのが、型が安定しているかどうかです。公式ドキュメントには「安定している」条件が記載されています*1 が、実装中に気づかずに不安定な型を使ってしまうこともあります。なぜかというと、この「安定している」かどうかはIDEのサポートで警告が表示されることもないため、実装者の経験値やそのときの状態によって意識せず使ってしまうからです。コードレビューで指摘できれば良いのですが、IDE上で確認が難しいものをブラウザ上で確認するのも大変です。
Strong Skipping Modeを使えば型の不安定は関係ない?
最近 Strong Skipping Mode というモードが公開されました。これを使えば型の安定のルールが緩和され、多くのComposable関数がSkippable関数として扱われるようになります。これにより再描画を抑えられるので、パフォーマンスは向上するように思いますが、今度は値が変わったにもかかわらず再描画されないリスクを抱えるようになるため、信頼しすぎるのもよくありません。
Compose Compiler Metrics
それでは、型の安定性に対してどのように向き合えばよいでしょうか。 実は Compose Compiler Metrics というものがあり、Compose Compilerが、Composable関数ごとにSkippableかどうか、使用しているパラメータの型が安定しているかどうかをレポートしてくれます。
例えばこれは、プロジェクト全体にあるComposable関数のうち、Skippableな関数が何個あるのかをサマリーしてくれたレポートです。 全体で8つのComposable関数があり、そのうち5つがSkippableであることが分かります。
{ "skippableComposables": 5, "restartableComposables": 8, "readonlyComposables": 0, "totalComposables": 8, "restartGroups": 8, "totalGroups": 11, "staticArguments": 5, "certainArguments": 5, "knownStableArguments": 50, "knownUnstableArguments": 2, "unknownStableArguments": 0, "totalArguments": 52, "markedStableClasses": 0, "inferredStableClasses": 1, "inferredUnstableClasses": 1, "inferredUncertainClasses": 0, "effectivelyStableClasses": 1, "totalClasses": 2, "memoizedLambdas": 4, "singletonLambdas": 0, "singletonComposableLambdas": 3, "composableLambdas": 3, "totalLambdas": 5 }
そしてこちらは、関数がSkippableなのか、パラメータが安定しているのかを知らせてくれるレポートです。ToggleButton
は Skippable ですが、ContactRow
はSkippableではないことがわかります。
restartable scheme("[androidx.compose.ui.UiComposable]") fun ContactRow( unstable contact: Contact stable modifier: Modifier? = @static Companion ) restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun ToggleButton( stable selected: Boolean stable onToggled: Function1<Boolean, Unit> ) restartable scheme("[androidx.compose.ui.UiComposable]") fun ContactDetails( unstable contact: Contact ) restartable skippable scheme("[0, [0]]") fun ExampleTheme( stable darkTheme: Boolean = @dynamic isSystemInDarkTheme($composer, 0) stable dynamicColor: Boolean = @static true stable content: Function2<Composer, Int, Unit> )
これがあれば、人それぞれの経験値に依存しないコードレビューが可能になります。CIで出力して確認すれば、コードレビューも効率化できそうです。
実際にコードレビューに使えるのか
実際にこのレポートをそのままコードレビューに使ってみたところ、いくつかの不満点が出てきました。
- ローカルで実行する必要があり、コードレビューのコストが増える
- そのPull RequestによってComposable関数の状態が良くなったのか悪くなったのかパット見で分からない
- プロジェクトには多くのComposable関数が含まれており、レポートを全て確認することが難しい
- SkippableではないComposableを見つけても、そのPRの主旨でない場合は改修できない
- 新規作成のComposable関数はSkippableにしたいが、既存のComposable関数の改善は後回しにしたい(が、レポートは膨大)
このレポートをそのまま使うと、本来確認したい情報以外も含まれ、コードレビューの負担になってしまいました。
本当にほしかったものを技術で解決する
そこで、danger-compose_compiler_metrics を作りました。このツールを使えば、Pull RequestでデフォルトブランチとのCompose Compiler Metricsの差分をとってレポートしてくれるため、必要な情報だけに絞ってコードレビューに活かすことができます。
実際に活用されている例
使い方を説明するためにサンプルリポジトリを作成したので、実際にPRにレポートされている様子を紹介します。
こちらのPR では Contact
クラスの変数がミュータブルで宣言されていることで、安定していない型となっていることを修正しています。コメント形式でCompose Compiler Metricsの差分がレポートされており、SkippableなComposable関数が増えたことが確認できます。レビュアーは一瞬で効果があることを理解できます。
danger-compose_compiler_metrics の使い方
danger-compose_compiler_metrics は名前からわかるとおり Danger のプラグインです。 すでにDangerを導入しているプロジェクトであれば簡単に導入できます。 Gemfileにgemを追記します。
gem 'danger-compose_compiler_metrics'
あとは Dangerfile で呼び出すだけです。「カレントブランチで作成したレポートのディレクトリ」と、「デフォルトブランチ(通常は main)で作成したレポートのディレクトリ」を指定すれば、Githubに差分をレポートしてくれます。以下のコードをそのまま使えば、多くのプロジェクトでうまく動作するでしょう。
Dir.glob('**/compose_compiler_metrics').each do |report_dir| next if report_dir.include?("vendor/bundle") compose_compiler_metrics.report_difference(report_dir, "#{report_dir}_baseline") end
なお、この方法を実現するにはデフォルトブランチのレポートが必要です。各CIサービスのキャッシュ機能を使うことで実現できます。Github Actionsを使った具体的な例は danger-compose_compiler_metrics-example にあるので、コードをぜひ参照してください。
おわりに
Compose Compiler Metricsをプロダクト開発に活用する実践的な方法を説明しました。STORES のモバイルチームでは少ないチームメンバーでもプロダクト開発できるように、課題を技術で解決するアプローチに積極的に取り組んでいます。もひ興味がある方がいらっしゃいましたら、ぜひ一緒にモバイルアプリ開発をしましょう。