FPS(Frames Per Second)は、グラフィックシステムが1秒間にレンダリングする個別のフレーム数を示すメトリクスです。モバイル開発において、FPSはUIパフォーマンスの標準的な指標であり、FPSが高いほどアニメーションが滑らかになり、インターフェースの応答性が向上します。Google Android Performance、2025年によると、モバイルアプリの目標FPSは毎秒60フレームであり、人間の目が動きを連続的で滑らかだと知覚する閾値です。
主要ポイント
FPS(Frames Per Second)は、コンピュータグラフィックス、ビデオ、モバイルインターフェースで使用されるフレームレートの測定単位です。各フレームは、短時間画面に表示される静止画像です。フレームが高速で切り替わると、脳はそれらを連続した動きとして認識します。この効果は視覚の残像効果と呼ばれます。モバイルアプリケーションにとって、FPSは重要なメトリクスです。なぜなら、フレームが1つでも欠落すると、滑らかなアニメーションが目立つカクつきに変わるからです。アプリケーションは各フレームを時間予算内に厳密にレンダリングする必要があります:60 FPSの場合は16.6 ms、90 FPSの場合は11.1 ms、120 FPSの場合は8.3 msです。
FPSはUIだけでなく、ゲーム、ビデオ、カメラでも測定されます。ゲームでは、FPSはシーンの複雑さ、テクスチャの品質、GPUの能力に依存します。ビデオでは、FPSは固定(24、30、60 fps)であり、コンテンツによって決定されます。モバイルアプリでは、FPSはUIコードの効率に依存します:レイアウトの複雑さ、ビューの数、再描画頻度、GC(ガベージコレクション)の動作です。Apple WWDC 2022によると、非効率的なコレクション更新(insert/delete/dequeueReusableCellの代わりにreloadData)により、アプリケーションの平均FPSは10~15%低下する可能性があります。リアルタイムでのFPS測定は、パフォーマンスに取り組むQAエンジニアや開発者にとって標準的なプラクティスです。
モバイルアプリケーションにおけるFPSの計算は、連続するフレーム間の時間を測定することに基づいています。最も単純な式:FPS = 1000 / deltaTimeMs。ここでdeltaTimeMsは、前のフレームの完了から現在のフレームの完了までの間隔です。現在のフレームが20 msでレンダリングされた場合、FPS = 1000 / 20 = 50です。ただし、実際にはFPSが1秒以内でも安定することはほとんどありません。典型的なプロファイルには、12~16 msのフレームに、スキップされた(jank)フレームや遅いフレーム(40~60 ms)が混在しています。そのため、FPSは1~5秒間の移動平均、またはフレーム時間分布のパーセンタイルとして測定されます。
Androidでは、FPSはChoreographerを介して計算され、VSync(ディスプレイ同期パルス)からコールバックを受け取ります。各コールバックは1つのフレームに対応します。コールバックが届かない場合、フレームはスキップされます。Choreographerを使用すると、1秒あたりの正確なフレーム数とスキップされたフレーム数を測定できます。iOSでは、CADisplayLinkが同様に機能します。ディスプレイが新しいフレームをレンダリングする準備ができるたびに呼び出されます。timestampプロパティには最後のフレームの正確な時間が含まれ、targetTimestampには次のフレームの予想時間が含まれます。それらの差が現在のフレームの時間予算です。
Swiftコードは、CADisplayLinkを介した簡単なFPS監視を示しています。frameCountカウンターは呼び出しごとに増加し、1秒に1回実際のFPSが計算されます。
class FpsCounter {
private var displayLink: CADisplayLink?
private var frameCount = 0
private var lastTime = TimeInterval(0)
func start() {
displayLink = CADisplayLink(
target: self,
selector: #selector(countFrame)
)
displayLink?.add(to: .current,
forMode: .common)
}
@objc
private func countFrame() {
frameCount += 1
let now = Date().timeIntervalSince1970
if now - lastTime >= 1.0 {
print("FPS: \(frameCount)")
frameCount = 0
lastTime = now
}
}
}
60 FPS(または60 Hz)標準が業界で確立された理由はいくつかあります。1つ目は生理学的理由:人間の目は50~60 Hzを超える周波数では個別のフレームを識別できず、滑らかな動きとして認識します。この閾値は臨界融合周波数(CFF)と呼ばれます。2つ目は歴史的理由:初期のブラウン管(CRT)は米国(NTSC)では60 Hz、欧州(PAL)では50 Hzで動作していました。最新のLCDディスプレイはこの周波数を継承しています。3つ目は工学的理由:UIアニメーションの場合、60 FPSはサブミリ秒のタッチ応答遅延を提供し、テキスト入力、スクロール、ドラッグに重要です。
モバイル開発者にとって、60 FPSは単なる推奨ではなく、1フレームあたり16.6 msの厳格な予算です。この予算はレンダリングのすべてのフェーズに分割されます:入力(1~2 ms)、アニメーション(2~3 ms)、レイアウト(3~5 ms)、描画(3~5 ms)、スワップ(1~2 ms)。いずれかのフェーズがサブ予算を超過すると、フレームが16.6 msに収まらない可能性があります。Google Android Performanceは、フレーム準備に12~14 ms以内に収め、システム割り込み(GC、バックグラウンドスレッド)用に2~4 msの余裕を残すことを推奨しています。Firebase Performanceによると、平均FPSが52未満、P99 FPSが30未満のアプリケーションは、Google Playのレビューでパフォーマンスに関する苦情が35%増加します。
FPSとフレーム時間は同じメトリクスの両面であり、混同しないことが重要です。FPSは速度、フレーム時間はレイテンシです。60 FPSでは、各フレームに16.6 msかかります。30 FPSでは33.3 msです。しかし、FPSは非線形メトリクスです:60から30 FPSへの低下はフレーム時間が2倍になったことを意味し、30から20への低下は1.5倍の増加を意味します。そのため、プロファイラは平均周波数ではなくフレーム時間を表示します。これにより、問題のあるフレームを確認できます。たとえば、平均55 FPSは、5%のフレームのフレーム時間が50~100 msであることを隠している可能性があります。これらのフレームはJankを引き起こしますが、平均FPSには大きな影響を与えません。
パフォーマンスを分析する際は、平均FPSではなくフレーム時間のヒストグラムを見ることをお勧めします。Android Studio ProfilerとiOS Instrumentsでは、フレーム時間はスケールで表示され、緑色ゾーンは16.6 ms(60 FPS)まで、黄色は16.6~33.3 ms(30~60 FPS)、赤色は33.3 ms以上(30 FPS未満)です。赤い列はそれぞれ、ユーザーにとって顕著な遅延です。実用的なルール:P95フレーム時間(95%のフレームがX ms以内に収まる)は、平均FPSよりも信頼性の高いメトリクスです。P95フレーム時間が32 ms(30 FPS)を超えると、平均FPSが50でもアプリケーションは遅く感じられます。
パーセンタイル付きでフレーム時間の配列をFPSに変換するKotlin関数。詳細な分析のために、平均FPSだけでなくP50、P90、P99も返します。
data class FpsReport(
val average: Float,
val p50: Float,
val p90: Float,
val p99: Float
)
fun List<Long>.toFpsReport(): FpsReport {
val fpsValues = this.map { ms ->
if (ms > 0) 1000f / ms else 0f
}.sorted()
return FpsReport(
average = fpsValues.average().toFloat(),
p50 = fpsValues[fpsValues.size / 2],
p90 = fpsValues[(fpsValues.size * 90 / 100)],
p99 = fpsValues[(fpsValues.size * 99 / 100)]
)
}
90、120、144 Hzのディスプレイを搭載した最新のモバイルデバイスは、FPSに新たな要件を課しています。アプリケーションが120 Hzディスプレイで60 FPSを提供する場合、画面更新の2サイクルごとに同じフレームが渡されるため、ユーザーはマイクロカクつきを認識します。120 FPSを維持するには、フレームあたりの予算が16.6から8.3 msに縮小され、2倍効率的なレンダリングコードが必要になります。Android開発者(Google I/O 2023)によると、安定した120 FPSを達成するには、描画サイクルでの割り当てを避け、階層内のビュー数を最小限に抑え(80未満)、重いdrawableをVectorDrawableに置き換え、複雑なグラフィックスにはsurfaceViewを使用する必要があります。
iOSの状況も同様です:ProMotion(120 Hz)を搭載したiPhone Proは2倍のフレーム数を必要としますが、フレームあたりの時間は半分になります。Appleは、すべてのアニメーションが120 FPSで動作する必要はないと指摘しています。Core Animationは、静的またはゆっくり変化する要素のフレームレートを自動的に下げます。ただし、スクロール、ジェスチャアニメーション、トランジションは、“なめらかな”感触のために120 FPSを提供する必要があります。60から120 FPSに移行する際の主な問題:消費電力の増加(GPUで25~40%)、デバイスの発熱、およびスロットリング(過熱によるフレームレートの低下)です。フォールバックメカニズムを実装することをお勧めします:フレーム時間が一貫して8.3 msを超える場合は、システムのスロットリングを待つ代わりに、プログラムで目標フレームレートを60 FPSに下げます。
Android用のJavaコードは、デバイスが120 FPSをサポートできるかどうかを判断し、レンダリングモードを切り替えます。サポートされているリフレッシュレートを判断するためにDisplay.getModeが使用されます。
class FpsModeSwitcher {
static boolean canDo120Fps(Activity activity) {
Display display = activity.getWindowManager()
.getDefaultDisplay();
for (Display.Mode mode : display.getSupportedModes()) {
if (mode.getRefreshRate() >= 120f) {
return true;
}
}
return false;
}
}
FPSの最適化には、プロファイリングから始まり、問題のある領域のリファクタリングで終わる体系的なアプローチが必要です。最初のステップは、プロファイラを使用して現在のFPSを測定することです。2番目のステップは、予算を超えるフレームを見つけることです。Androidでは、GPU ProfilingまたはPerfettoを介してこれを行うことができます。iOSでは、Core Animationテンプレートを使用したInstrumentsを使用します。3番目のステップは、原因を排除することです:オーバードローの削減、ビュー階層の深さの低減、レイアウトフェーズのConstraintLayoutへの置き換え、ViewHolder Recyclingの追加、重い計算のバックグラウンドスレッドへの移動。
FPS固有の最適化には以下が含まれます:フレームペーシング — 高速フレームと低速フレームの“バースト”を避けるために、フレーム間の時間を均等に分散するメカニズム。Androidでは、固定間隔のChoreographer.FrameCallbackを使用してフレームペーシングを実装できます。iOSでは、CADisplayLink.preferredFrameRateRangeが同じことを行います。2番目の方法はトリプルバッファリング:システムは2つではなく3つのバッファを使用し、GPUが前のフレームの解放を待たずに次のフレームのレンダリングを開始できます。Androidは必要に応じて自動的にトリプルバッファリングを有効にしますが、iOSでは開発者がCAMetalLayerを介して明示的に要求できます。3番目はテクスチャキャッシング:毎フレームの再読み込みを避けるために、GPUメモリにビットマップをキャッシュします。
固定間隔16.6 msでのフレームペーシングの実装を示すKotlinの例。システムが遅延しても、すべてのコールバックは均一な間隔で届きます。
class PacedFrameRenderer {
private val targetDelta = 16_666_666L // 16.6 ms (60 FPS)
private var lastFrameTime = 0L
private val frameCallback =
Choreographer.FrameCallback { frameTimeNanos ->
val delta = frameTimeNanos - lastFrameTime
if (delta >= targetDelta) {
onFrame(delta)
lastFrameTime = frameTimeNanos
}
Choreographer.getInstance()
.postFrameCallback(this)
}
private fun onFrame(delta: Long) {
// フレームレンダリング
}
}
よくある質問
60 FPSはモバイルアプリにとって快適なレベルです。60と120 FPSの違いは、高速アニメーション(スクロール、ドラッグ)中の高リフレッシュレートディスプレイでのみ認識できます。30 FPS未満では不快感があります。
FPS = 1000 / FrameTime(ms)。フレーム時間 = 16.6 msの場合、FPS = 60。フレーム時間 = 33.3 msの場合、FPS = 30。FPSではなくフレーム時間を監視することをお勧めします。問題のあるフレームが表示されるからです。
スクロール中、システムはリストの新しいアイテムごとにLayoutとDrawを呼び出します。ビューが複雑である、レイアウトがキャッシュされていない、または重いdrawableが使用されている場合、フレーム時間が増加しFPSが低下します。解決策はViewHolderのリサイクルとフラットな階層です。
Core Animationテンプレートを使用したInstrumentsを使用します(リアルタイムでFPSを表示)。プログラムによる測定には、1秒あたりのフレーム数をカウントするCADisplayLinkを使用します。本番環境では、MXAnimatoryMetricメトリクスを使用したMetricKitを使用します。
トリプルバッファリングは2つではなく3つのバッファを使用し、GPUが現在のVSyncの完了前に次のフレームのレンダリングを開始できるようにします。これによりピーク負荷が平滑化され、FPSの安定性が向上しますが、1フレームのレイテンシが追加されます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。