Compose パフォーマンスを年末に振り返ってみた

published
published
author
authorDisplayName
Akiko Tomonaga
category
android
mainImage
20251222_akky_001.png
publishedAt
Dec 22, 2025
slug
Advent-Calendar-20251222
tags
advent calendar
Android
notion image
 
💡
この記事は、「NEWT Product Advent Calendar 2025」Day16 および「Android Advent Calendar 2025」Day22 の記事となります。
NEWT Product Advent Calendar 2025」16日目は、令和トラベル Androidアプリエンジニアのともながが、執行役員CPO magaraからバトンをもらい執筆。ぜひ、最後までご覧ください!
 

はじめに

旅行アプリ『NEWT(ニュート)』は、立ち上げ当初から Jetpack Compose を前提に開発しており、UI実装も Compose ベースで安定して行っています。
 
2025年は機能開発のサイクルが非常に速く、プロダクトとして実現したいアイデアを次々と形にしていく一年でした。
開発中に「このUI、もう少し改善できるかも?」と感じる場面は何度かあったものの、体感として大きな問題が出ているわけではなく、まずはカスタマー価値につながる機能を優先して実装を進めてきました
 
そんな背景もあり、作った画面のパフォーマンスを丁寧に振り返る機会はあまり取れていませんでした。
そこで今年の締めくくりとして、今年実装したいくつかの画面を対象に、Compose のパフォーマンスを改めて確認してみることにしました!
 

調査対象の画面

今年新規実装した画面、状態管理や UI の構造が今年大きく変わった画面をターゲットとして、下記を選びました。
  • ツアー一覧画面
  • エリートプログラム画面
 
どちらも問題なく動いているように見えますが、実際はどうでしょうか。あらためて客観的に計測してみることにします。
 
💡

今回の目的

  1. 現状の Compose UI がどの程度効率よく動作しているかを客観的に把握する
  1. 普段意識しているパフォーマンス配慮が、実際の挙動として効果を発揮しているか検証する
  1. 今後の開発サイクルに組み込める改善ポイントを整理し、UI 品質の継続的な向上につなげる
 
高速に機能開発を進めていく中でも、パフォーマンス観点での気づきを継続的に取り入れていけるようにしたい。そのための土台づくりが今回の狙いです。
 

計測方法

今回の計測では、Compose のパフォーマンスの中でも「状態更新がどこまで再コンポーズを引き起こしているか」という構造的な部分にフォーカスしました。
Android Studio の Layout Inspector を使い、再コンポーズの発生箇所や影響範囲を中心に確認していきます。
コードにログを仕込むなどの計測は行わず、実装に手を加えない形での検証としています。
  • Layout Inspector で見る内容
    • 再コンポーズの発生箇所
    • Composable ツリー構造の把握
    • 状態更新による影響範囲
 

ツアー一覧画面

ツアー一覧画面は、カスタマーが最も多く閲覧する画面のひとつです。
よりニーズに合ったツアーを素早く見つけられるよう、検索体験の改善を目的に、細かなチューニングを日々行っています。
 
今年は特に、しぼりこみフィルターの拡充やUIの整理を中心に手を入れてきました。条件の追加や操作フローの見直しなどを重ね、検索のしやすさを継続的に改善しています。
よく使われるフィルター項目はリスト画面上で設定できます。
よく使われるフィルター項目はリスト画面上で設定できます。
より詳細なフィルターやならび順かえも設定できます。
より詳細なフィルターやならび順かえも設定できます。
 

Before:初期計測で見えてきたこと

まずは改善前の状態をそのまま計測し、現状を把握するところから始めます。
Layout Inspector をオンにして実際の操作を見てみます。
ぱっと見では特に違和感はありませんが、Inspector 越しに挙動を追っていくと、いくつか気になるポイントが見えてきました。
 
事例①:スクロール時にフィルターエリアにも再コンポーズが発生する
まず分かりやすいのは、リストのスクロール操作です。ツアー一覧を上下にスクロールすると、表示上は変化していないはずのしぼりこみフィルターにも再コンポーズが発生している様子が確認できました。
Recomposition countsのビューで見ても、SearchTourFilters をラップする Surface がカウントされ続けているのがわかります。
 
 
事例②:お気に入りトグル時に全カードが再コンポーズされる
続いて、カードのお気に入りボタンをタップした場合です。エミュレーター画面上のオーバーレイでは画面全体が反応しているような挙動で気になりますが、特に気になるのは、タップした以外のカードにも再コンポーズが走っている点です。
操作するたびに、全ての SearchTourItem で Recomposition counts が増えています。
 
 
 

After:改善後の挙動

原因を整理した上で実装を調整し、再度 Layout Inspector で挙動を確認しました。
 
改善①:スクロール時にフィルターエリアにも再コンポーズが発生する
スクロール位置に応じてフィルタエリアの Surfaceelevation を切り替える処理を行っていたのですが、コードを追っていくと、Surfaceelevation は Composition フェーズで評価されるため、スクロールのたびに再コンポーズを引き起こしていることが分かりました。
Before:
val needsElevation = remember { derivedStateOf { listState.firstVisibleItemScrollOffset >= threshold } } Surface( elevation = if (needsElevation.value) AppBarDefaults.TopAppBarElevation else 0.dp, ) { Filters(...) }
 
調べてみると、graphicsLayer を使うことで elevation を表現しつつ、状態の読み取りを描画フェーズで行えることが分かりました。
これにより、スクロール位置の state を Composition フェーズで読まない構造にでき 、結果としてスクロール時に不要な再コンポーズが発生しない状態になりました。
After:
Surface( modifier = modifier.graphicsLayer { val needsElevation = listState.firstVisibleItemScrollOffset >= thresholdState.floatValue shadowElevation = if (needsElevation) elevationPx else 0f } ) { Filters(...) }
 
Layout Inspectorの結果:
 
 
SearchTourFilters をラップする Surface でのカウントが発生しなくなりました 🎉
 
 
改善②:お気に入りトグル時に全カードが再コンポーズされる
Before の実装では、お気に入り操作中のアイテムID(togglingItemId)をリスト内のすべての SearchTourItem に渡していました。
これは、APIリクエスト中のダブルタップを防ぐことを目的としたもので、各アイテム側で「自分が対象かどうか」を togglingItemId から判定する構造になっています。
そのため、Compose から見ると全アイテムが同じ state を受け取っている状態になり、「どのアイテムに変化があるか」を検知できず、必要以上に再コンポーズが広がっていたようです。
Before:
SearchTourItem( item = item, togglingItemId = uiState.togglingItemId, ){ ... val isToggling = togglingItemId == item.id IconButton( enabled = !isToggling, ... ) )
そこで比較処理を親側に移し、子にはBooleanのみを渡す構造に変更しました。
 
After:
SearchTourItem( item = item, isToggling = uiState.togglingItemId == item.id, ){ ... IconButton( enabled = !isToggling, ... ) )
これにより、変更があったアイテムだけを明確に識別できるようになり、再コンポーズが正しく働くようになりました 🎉
 
Layout Inspectorの結果:
 
 
タップした以外の SearchTourItem では、 再コンポーズがSkipされているのがわかります👏
 
ツアー一覧画面では、スクロールやタップといった日常的な操作が想定以上に再コンポーズを広げているケースが確認できました。同じ観点で、別の画面でも挙動を確認してみます。
 

エリートプログラム画面

エリートプログラムは、今年あたらしく始まった、ステータスに応じたさまざまな特典を利用できる会員プログラムです。
こちらの画面は、横スワイプで各ステータスを切り替えたり、進捗計算やアイコンのアニメーション入ったりとリッチなUIになっています。
notion image
notion image
 

Before:初期計測で見えてきたこと

Layout Inspector をオンにして実際の操作を見てみます。
グラデーションの表現やアニメーションも入っているので、比較的再コンポーズが起きやすい画面ではありますが、一番気になったところはタイトルバーです。
 
事例③:スクロール時にタイトルバーが更新され続ける
タイトルバーは、見やすさの観点から、スクロール位置が一番上にある場合と、スクロール途中とで、文字やアイコンの色を変化させています。一度スクロールが始まると色の変化はないですが、 TopAppBarTopBarTitle の Recomposition counts が増え続けていているのがわかります。
 
 

After:改善後の挙動

改善③:スクロール時にタイトルバーが更新され続ける
こちらもスクロール操作がトリガーとなる再コンポーズの問題ですが、ツアー一覧画面とは少し違い、原因は比較的シンプルなものでした。
Before の実装では、scrollState.value を直接参照して isScrolled を算出し、その結果を使ってタイトルバーの表示を切り替えてしまっていました。
Before:
val isScrolled = scrollState.value > 0 val targetTopBarTextColor = if (isScrolled) { defaultTextColor } else { remember(rank) { rank.textColor } }
 
scrollState.value はスクロール中に連続的に変化する値のため、その参照が Composition フェーズで行われていると、スクロールに追従する形でタイトルバー周辺の再コンポーズが発生します。
今回のケースでは「スクロール中かどうか」という判定結果のみが分かれば十分だったため、derivedStateOf を用いて isScrolled を Compose の state として定義しました。
これにより、スクロール中の細かな値変化では再コンポーズが発生せず、true / false が切り替わるタイミングにのみ更新されるようになりました。
 
After:
val isScrolled by remember { derivedStateOf { scrollState.value > 0 } } val targetTopBarTextColor = if (isScrolled) { defaultTextColor } else { remember(rank) { rank.textColor } }
 
Layout Inspectorの結果:
 
 
スクロール中に TopBarTitle のカウントが増えなくなりました 🎉
 

改善してみた気づき

今回の改善では、既存の知識を改めて整理できた部分と、実装してみて理解がより深まった部分がそれぞれありました。
 
  1. Composableに渡す引数の設計で再コンポーズの範囲が決まる
    1. Composable にどのような値を引数として渡すかによって、再コンポーズの発生範囲は大きく変わることを改めて実感しました。
      ツアー一覧画面では、すべてのアイテムが同じ引数を受け取る構造になっていたため、状態更新のたびに、関係のないアイテムまで再コンポーズ対象になっていました。一方で、「この UI が本当に必要としている判定結果」まで state を絞り込んで渡すことで、変更のあった箇所だけを Compose が正しく識別できるようになりました。
       
  1. state を「どのフェーズで読むか」を意識すると改善ポイントが見つかりやすい
    1. スクロール位置のように連続的に変化する state を Composition フェーズで直接参照していると、UI上は変化していなくても再コンポーズが発生し続けるケースがあります。
      今回の改善では、derivedStateOf を使って判定結果を state として安定させたり、graphicsLayer を利用して描画フェーズで完結させることで、UIの構造を変えずに不要な再コンポーズを抑えることができました。
      特に、描画フェーズに処理を寄せるという点は、今回の改善の中でも個人的に一番「なるほど」と感じたポイントです。
 

まとめ

今回の調査では、「状態の変化が本来関係ない UIにまで伝搬してしまう」というCompose ではわりと起こりがちなパターンがいくつか見つかりました。
 
大きなリファクタを行わずとも、
  • 状態をどこで読むか
  • どのフェーズで UI を更新するか
  • Smart Recomposition が正しく動けるデータ構造か
といった基本的なポイントを整理するだけで、Composeパフォーマンスの改善につながることが分かりました。
 
現在、令和トラベルでは iOS と Android のチームを統合し、ひとつのアプリチームとして開発を進めています。実装者の習熟度に関わらず、同じ前提で設計や実装の判断ができる状態をどう作るかは、最近特に意識しているテーマのひとつです。
▼ ネイティブアプリチームの体制についてはこちら:
 
その一環として、AI coding agent のルール整備を日々行っているのですが、今回見つかったような、再コンポーズが広がりやすい書き方や state の扱い方についても、agent に指定するルールとして整理していけそうだなと感じました。人によって判断がブレやすいポイントを仕組みで補いながら、UI 実装の前提を揃えていくことができそうです。
今後も高速に開発が進んでいく中で、今回のような振り返りを定期的に行い、UIの品質向上につなげていければと思います!
 
 

📣 1月のイベント開催のお知らせ

令和トラベルでは、毎月技術的な知識や知見・成果を共有するLT会を毎月実施しています。発表テーマや令和トラベルに興味をお持ちいただいた方は、誰でも気軽に参加いただけます。
 

【1/28 開催!3社共催】モバイルアプリ開発 ✕ AI ー 組織・技術課題と向き合い、AIと走る

2026年のスタートを切る1月の「NEWT Tech Talk」は、”モバイルアプリ開発 ✕ AI ー 組織・技術課題と向き合い、AIと走る” というテーマで開催。
クラシル株式会社 なぐもさん、株式会社ヤプリ にゃふんたさんをゲストに、令和トラベル やぎにいの3名が登壇します。モバイルアプリ開発の現場で、AI活用に取り組む3社のエンジニアが、個人・チーム・技術課題それぞれの視点から、AIとどのように向き合い、どのように開発を前に進めてきたのか、具体の取り組みをシェアしながら語ります!
 
そのほか、毎月開催している技術発信イベントについては、connpass にてメンバー登録して最新情報をお見逃しなく!
 

【NEWT Chat リリース記念】AI × Travel Innovation Week 開催!

「NEWT Chat」誕生の裏側や開発ストーリーをお届けする特別企画 “AI × Travel Innovation Week” を令和トラベルのnote上で開催しました!
「NEWT Chat」のリリース背景、プロダクトの価値、開発体制、そして今後の展望など、新規事業の “舞台裏” を公開。特に、AIプロダクト開発に関わるエンジニア・PMの皆さまにとって学びの多い内容となりますので、ぜひご覧ください。
▼ AI × Travel Innovation Week のnoteはこちら:
 
旅行・観光業に特化したAIエージェントチャット「NEWT Chat(ニュートチャット)」についてはこちらから。
 

令和トラベルでは一緒に働く仲間を募集しています

この記事を読んで会社やプロダクトについて興味を持ってくれた方は、ぜひご連絡お待ちしています!お気軽にお問い合わせください!
フランクに話だけでも聞きたいという方は、カジュアル面談も実施できますので、お気軽にお声がけください。
 

1年間の感謝を込めた、”クリスマスセール🎄” 開催中!

NEWTでは現在、海外旅行やホテルをおトクにご予約いただける『クリスマスセール🎄』を12/4〜スタートしています!ぜひこの機会にご利用ください!
 

📣宣伝

次回のNEWT Product Advent Calendar 2025Day17は、「Playwright Test Agents × Claude Code Skillsで実現するプロダクション品質のE2Eテストコード自動生成」と題してQAエンジニアのOsuが担当します。次のブログもお楽しみに!

# advent calendar

# Android