ClaudeExpo2026/04/28 13:30

The real cost of React Native animations: benchmarking every approach

要点だけを先に読めるように短く再構成したセクションです。

元記事

Quick Digest

要約

要点だけを先に読めるように短く再構成したセクションです。

claudejamodel: claude-haiku-4-5

React Nativeアニメーションライブラリの実装コスト比較:4つのアプローチをベンチマーク

Key Points

  • iOSでEaseはゼロオーバーヘッド、Reanimatedは常時UIスレッド負荷
  • デバッグビルドは信頼できない、リリースビルドで検証必須
  • 500ビューではEaseのみフレームバジェット内、Reanimatedは36ms超過

Summary

React Nativeアニメーションライブラリの選択がアプリケーションパフォーマンスに与える影響を定量的に測定した包括的なベンチマーク調査。Ease、Reanimated(Shared Values/CSS Animations)、RN Animatedの4つのアプローチを、iOS/Android両プラットフォーム、複数デバイスで比較し、フレームごとのUIスレッドオーバーヘッドを追跡。

Key Points

  • プラットフォーム間の大きな差異: iOSではEaseが~0.01msで実質ゼロオーバーヘッド(Core Animationが別プロセスで実行)、一方Reanimatedは常にUIスレッドで動作
  • ビルド設定の影響が最大: デバッグビルドはリリースビルドより大幅に遅く、Reanimatedは50ビューでフレームバジェット(16.67ms@60fps)を超過。リリースビルドでは11msに改善
  • スケーリング特性: 10~100ビューではすべてのアプローチがバジェット内だが、500ビューではEaseのみ対応可能。Reanimated SVは36msに達する
  • シャドウツリーの税金: Reanimatedは毎フレーム値を計算し、レイアウト効果のないプロパティ(transform/opacity)更新でもYogaレイアウトパスを実行。フィーチャーフラグで11~19%削減可能
  • 実用的な使い分け: 長時間実行アニメーション(スケルトンローダー、背景エフェクト)と低エンドデバイスで選択が重要。短時間トランジションではオーバーヘッド無視可能
  • React Native 0.85の改善: Shared Animation Backendがシャドウツリーバイパスをデフォルト化し、最適化フラグが不要に。ただしEaseの構造的優位性は残存
  • 測定方法: iOS(CADisplayLink swizzling)とAndroid(Window.OnFrameMetricsAvailableListener)で正確なフレームメトリクスを取得。リポジトリのベンチマークアプリで再現可能

Full Translation

翻訳

原文の流れを保ったまま読める翻訳セクションです。

claudejamodel: claude-haiku-4-5

React Native アニメーションの実際のコスト: すべてのアプローチのベンチマーク

これは App&Flow のコンサルティング責任者であり、長年の React Native コントリビューターである Janic Duplessis からのゲスト投稿です。

ログイン画面にゆっくり漂う背景があるシーンを想像してください。製品が洗練されていると感じさせるような微妙なモーションです。十分にシンプルです。Reanimated で実装して出荷しました。しかし時々、フレームドロップが発生します。わずかですが、何か違和感を感じさせるほどです。一度気づくと気になるようなものです。根本原因は、Reanimated がフレームごとに UI スレッドで実行されることです。アプリがそのフレーム中に重要な処理を行う場合(再レンダリングが発生、リストがスクロール、入力が更新される)、アニメーション予算が縮小し、その遅いバックグラウンド移動が遅いバックグラウンドスタッターになります。

App & Flow では、詳細にこだわる製品チーム向けの React Native アプリとツールを構築しています。流動的でネイティブのような UI は、その大きな部分です。問題を回避する代わりに、より良いアプローチを探しました。

iOS の Core Animation はアニメーションを OS レンダーサーバーに渡し、二度とスレッドに触れません。CAAnimation を与えると、システムがそれを駆動し、アプリはループから完全に外れます。React Native でそれが欲しかったのです。

それが react-native-ease が生まれた理由です。プラットフォーム API(iOS の Core Animation、Android の ObjectAnimator)を通じてすべてを駆動する宣言的アニメーションライブラリで、JS ループ、ワークレット、フレームごとのシャドウツリーコミットはありません。

しかし、それを構築することで、正直に答えたい質問が生じました。アニメーションライブラリの選択は実際にどの程度重要なのか?

そこで測定しました。4 つのアプローチ、2 つのプラットフォーム、ハイエンドとミッドレンジデバイスの両方にわたって、フレームごとの UI スレッドオーバーヘッドを追跡しました。このポストは、私たちが見つけたものを共有し、実際に重要な質問に答えようとします。フレームペナルティはどの程度大きいのか?どのような種類のアプリで重要なのか?アニメーションライブラリを選択する際に何を優先すべきか?

💡 Reanimated 側のフレームドロップはシミュレートされています。人工的な UI スレッド圧力を注入して、ビジーなアプリで何が起こるかを再現しました。実際のジャンクは、ワークロード、デバイス、UI スレッドで同時に何が起こっているかに依存します。

テストされた 4 つの React Native アニメーションライブラリ

すべてのベンチマークは 2026 年 4 月に Expo SDK 55、React Native 0.83、Reanimated 4.3.0、react-native-ease 0.7.0 で実行されました。4 つのアニメーションアプローチを比較しました。

  • Ease: react-native-ease、プラットフォーム API を直接使用。アニメーションは JS 側でプロップとして記述され、フレームごとの JS 関与なしでネイティブに駆動されます。
  • Reanimated (Shared Values): 標準的なワークレットベースのアプローチ。値は C++ ワークレットランタイムを介して UI スレッドで駆動されますが、各フレームはシャドウツリーを通じてプロップを更新します。
  • Reanimated (CSS Animations): Reanimated の新しい CSS アニメーション API。Ease のように宣言的ですが、Reanimated のアニメーションエンジンに支えられています。
  • RN Animated: React Native の組み込み Animated API(useNativeDriver: true)。値はネイティブに駆動されますが、実装はプラットフォームによって異なります。

また、Reanimated のスタティック機能フラグを有効にしてテストしました。特に ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPS と IOS_SYNCHRONOUSLY_UPDATE_UI_PROPS は、非レイアウトプロップ(transform と opacity など)のみが更新される場合、Reanimated がシャドウツリーコミットをスキップできます。別途呼び出す価値のある意味のある最適化です。

注: RN 0.85 は新しい Shared Animation Backend を導入しました。これにより、最終的に機能フラグが不要になります。Reanimated の統合は進行中ですが、まだリリースされていません。

ベンチマークがフレームごとのオーバーヘッドをどのように測定するか

ループ内で N 個のビューを同時にアニメーション化するベンチマーク画面を構築しました(translateX、2s、linear、repeating)。カスタム Expo ネイティブモジュールを使用してフレームごとのオーバーヘッドを測定しました。

  • iOS: CADisplayLink のファクトリメソッドをスウィズルして、任意のフレームワークによって登録されたすべてのディスプレイリンクコールバックをインターセプトし、フレームタイムスタンプで集約されたコールバックごとのウォールクロック時間を測定します。
  • Android: Window.OnFrameMetricsAvailableListener を使用します。これはプラットフォームのフレームメトリクスシステムから ANIMATION_DURATION、LAYOUT_MEASURE_DURATION、DRAW_DURATION をレポートします。

テストごとに 5 秒間の収集ウィンドウと複数の構成を実行して、Reanimated のワーストケースとベストケースのパフォーマンスの両方を示しました。

ベンチマーク結果: iOS と Android でのフレームごとの UI スレッドコスト

Android (Moto G8 Plus)

Android はライブラリ間の最も直接的な比較です。すべてのアプローチが UI スレッドで実行されるため、各アニメーションエンジンがフレームごとに追加する作業量の直接的な測定値です。トリックやショートカットはありません。

ビルド構成はどの程度重要か?(50 ビュー、平均 ms)

Reanimated パフォーマンスの最大の変数は、どのアニメーション API を選択するかではありません。デバッグビルドかリリースビルドかでテストしているかどうかです。

赤い線は 60fps での 16.67ms フレーム予算です。デバッグモードでは、Reanimated SV と CSS の両方が、わずか 50 ビューでそれを超え、積極的にフレームをドロップします。リリースビルドの同じアニメーションは 11ms で実行されます。デバッグビルドは嘘をつきます。開発中にアニメーションジャンクに気づいた場合、パニックになる前にリリースビルドで再現してください。

機能フラグは、非レイアウトプロップのシャドウツリーコミットをバイパスすることで、さらに 11~19% を追加します。一部のアプリで視覚的なバグを引き起こす可能性があるため、オプトインですが、オーバーヘッドが見られる場合はテストする価値があります。

ビュー数でオーバーヘッドはどのようにスケーリングするか?(リリース、すべての FF、平均 ms)

💡 500 ビューはストレステストであり、現実的なターゲットではありません。500 個のものを同時にアニメーション化している場合、アニメーションライブラリが最大の問題ではないかもしれません。

10~100 ビューでは、すべてのアプローチが平均的にフレーム予算を下回りますが、Reanimated と RN Animated は 100 ビューでそれから 5ms 以内であり、フレーム作業の残りのためのヘッドルームがほとんどありません。

500 ビューでは、Ease だけが予算を下回ります。Reanimated SV は 36ms に達し、フレーム予算の 2 倍以上です。これは最適化された構成です。

iOS (iPhone 15 Pro)

iOS は、アーキテクチャの違いが無視できなくなる場所です。Android では、すべてのライブラリが UI スレッドを共有するため、比較は公平です。iOS では、Ease は(最良の方法で)カンニングできます。Core Animation は別の OS レンダーサーバープロセスで実行され、アプリの完全に外側です。Ease が CAAnimation を登録すると、システムが引き継ぎ、スレッドは他の作業を自由に行えます。

そのため、Ease は全体で ~0.01ms を示します。UI スレッドで本当に何も起こっていません。

トレードオフは、Core Animation アニメーションを JS から読み取ったり、飛行中に中断したりできないことです。これは、ジェスチャー駆動アニメーションが依然として Reanimated に属する理由です。

ディスプレイリンクコールバック時間(フレームごと、ms、リリースビルド)

絶対数は Android より低いです。測定は UI スレッドコールバック時間のみをキャプチャするためです。しかし、ポイントは変わりません。iOS では、Ease はアニメーション化されているビュー数に関係なく UI スレッドコストを追加しませんが、他のすべてのアプローチはフレームごとに作業を続けます。

React Native アニメーションライブラリがフレームごとのコストで異なる理由

シャドウツリータックス

フレームごとに、Reanimated のワークレットは新しい値を計算し、シャドウツリーを通じてプロップ更新をコミットします。そのコミットは Yoga レイアウト、プロップ diffing、ビュー変更を実行します。transform または opacity(レイアウトに影響を与えないプロパティ)をアニメーション化する場合、その作業のすべてが無駄です。レイアウト パスの全価格を支払って、ブロブを 3 ピクセル左に移動します。Yoga は知る必要がありません。

機能フラグ(ANDROID/IOS_SYNCHRONOUSLY_UPDATE_UI_PROPS)は、視覚的なプロップ更新をレイアウトパスをスキップして UI レイヤーに直接プッシュすることで、これをショートカットします。Moto G8 Plus の 50 ビューでは、Reanimated SV を 11.87ms から 10.57ms(-11%)に削減し、CSS を 11.20ms から 9.06ms(-19%)に削減しました。一部のアプリで視覚的なバグを引き起こす可能性があるため、オプトインですが、オーバーヘッドを追跡している場合は、最初に試すべきことです。

RN Animated

RN Animated(useNativeDriver: true)は、フレームごとに JS スレッドもスキップしますが、アニメーション化されたノードごとに簿記オーバーヘッドを持つ別のネイティブアニメーションモジュールを通じてアニメーションを駆動します。低~中程度のビュー数では問題なく機能しますが、アニメーション化されたビューの数が増えると、Reanimated CSS よりも悪くスケーリングします。これは部分的には、機能フラグが有効にするシャドウツリー最適化がないためです。

アニメーションライブラリの選択が本番アプリで重要な場合

スケルトンローダー、漂うバックグラウンド、環境 UI エフェクトなど、長時間実行または遅いアニメーションで最も重要です。5 秒のアニメーションで 1 フレームがドロップされるのは目立ちます。他の作業(データ取得、再レンダリング、ユーザーインタラクション)はほぼ常に同時に発生しています。

また、リスト内のすべてのもの、画面上に数百のアニメーション化されたアイテムを簡単に持つことができるものにも重要です。低エンドデバイスでは、小さなフレームごとのオーバーヘッドが速く複合し、ユーザーはあなたの前に気づきます。

短い 1 回限りのトランジション(ボタン押下、トースト、モーダル)の場合、オーバーヘッドは無視でき、どのライブラリでも問題なく機能します。

注目すべき点: Ease はこの特定のユースケースのみをカバーしています。ジェスチャー駆動アニメーション(スクロールリンク、ドラッグ、スワイプ)とレイアウトプロパティ(幅、高さ、パディング)を変更するものは、依然として Reanimated または RN Animated が必要です。Ease は、視覚的なプロパティの宣言的でトリガーベースのアニメーション用に目的構築されています。

React Native 0.85 と Shared Animation Backend

React Native 0.85 は、Meta と Software Mansion によってレンダラーに直接構築された統一アニメーションエンジンである実験的な Shared Animation Backend を出荷します。Reanimated の統合が出荷されると、SYNCHRONOUSLY_UPDATE_UI_PROPS は不要になります。シャドウツリーバイパスがデフォルトパスになり、「デフォルト Reanimated」と「最適化された Reanimated」の間のギャップが事実上消えるためです。

アーキテクチャの違いは残ります。Ease にはフレームごとのアニメーションエンジンがまったくありません。より高速なバックエンドでも、Reanimated は依然として値を計算し、フレームごとにプロップ更新をプッシュします。そのオーバーヘッドは消えません。ただ、小さくなるだけです。

統合が出荷されたら、ベンチマークを更新します。

React Native アニメーションベンチマークを自分で実行する

ベンチマークはサンプルアプリに組み込まれています。リポジトリをクローンし、yarn example ios または yarn example android を実行し、デモ画面から Benchmark をタップします。ソースは example/src/demos/BenchmarkDemo.tsx にあり、ネイティブモジュールは example/modules/frame-metrics/ にあります。

1 つの注意: リリースビルドを使用してください。デバッグモードは Reanimated の数を大幅に膨らませます。したがって、数字が警告的に見える場合、それはおそらくその理由です。

yarn example ios --configuration Release
yarn example android --variant release

react-native-ease は App & Flow によって構築されています。App & Flow は、Expo によって推奨されるモントリオールベースの React Native エンジニアリングスタジオです。