React Native アニメーションの実際のコスト:各手法をベンチマークする

React Native のアニメーションライブラリは、1 フレームあたり実際どれだけのコストがかかるのか。実機の iOS と Android で Ease、Reanimated、RN Animated をベンチマークした。

日本語
コピー
The real cost of React Native animations: Benchmarking every approach

この記事は Janic Duplessis によるゲスト投稿です。同氏は App&Flow でコンサルティング事業を率い、React Native の長期的なコントリビューターでもあります。

ログイン画面を思い浮かべてほしい。背景がゆっくりと流れていく、あの繊細な動きだ。プロダクトが作り物ではなく、丁寧に仕上げられているように見える。実装は簡単に思えた。Reanimated で作ってリリースした。ところが時々、1 フレーム落ちる。一度気づいてしまうと、もう無視できない程度には。

原因は単純で、Reanimated は毎フレーム UI スレッドで動く。アプリがそのフレームで大量の処理を抱えていると——再レンダリング、リストのスクロール、入力の更新——アニメーションに割ける予算が削られ、あのゆっくりした背景の動きが、ゆっくりした背景のカクつきに変わる。

App & Flow では、細部にこだわるプロダクトチーム向けに React Native アプリとツールを作っている。滑らかでネイティブらしい UI はその重要な要素だ。だからこの問題を回避するのではなく、もっと良い方法を探すことにした。

iOS の Core Animation はアニメーションをシステムのレンダーサーバーに委ね、その後はあなたのスレッドを一切使わない。CAAnimation を一度渡せば、あとはシステムが引き受け、アプリはそのループから完全に外れる。これを React Native でもやりたかった。そこで生まれたのが react-native-ease だ。すべてをプラットフォーム API で駆動する宣言的アニメーションライブラリで(iOS は Core Animation、Android は ObjectAnimator)、JS ループも worklet も、毎フレームの shadow tree コミットもない。

ただ、作る過程で、正直に答えたい問いが浮かび上がった。アニメーションライブラリの選択は、いったいどれほど影響するのか?

そこで計測した。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 側で props として記述し、ネイティブが駆動する。毎フレーム JS は関与しない。

  • Reanimated(Shared Values): worklet ベースの標準的な手法。値は C++ worklet ランタイムが UI スレッドで駆動するが、毎フレーム shadow tree 経由で props を更新する必要がある。

  • Reanimated(CSS Animations): Reanimated の比較的新しい CSS アニメーション API。Ease と同様に宣言的だが、内部は依然として Reanimated のアニメーションエンジン。

  • RN Animated: React Native 標準の Animated API を useNativeDriver: true と組み合わせて使う。値はネイティブが駆動するが、実装はプラットフォームによって異なる。

さらに、静的 feature flags を有効にした Reanimated もテストした。具体的には ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPSIOS_SYNCHRONOUSLY_UPDATE_UI_PROPS で、非レイアウト系の props(transformopacity など)だけを更新するときに Reanimated が shadow tree コミットを省略できるようにするものだ。これは単独で取り上げる価値のある、本質的な最適化だ。

注意: RN 0.85 で新しい Shared Animation Backend が導入され、最終的にはこれらの feature flags は不要になる。Reanimated 側の対応は進行中だが、まだリリースされていない。

ベンチマークが毎フレームのコストをどう測るか

サンプルアプリにベンチマークページを組み込み、N 個のビューを同時にアニメーションさせてループさせた(translateX、2 秒、リニア、リピート)。毎フレームのコストは自作の Expo ネイティブモジュールで計測している。

  • iOS: CADisplayLink のファクトリメソッドを swizzle し、フレームワークが登録したすべての display link コールバックを横取りして、フレームのタイムスタンプごとに集計し、各コールバックのウォールクロック時間を測る。

  • Android: Window.OnFrameMetricsAvailableListener を使い、プラットフォームのフレームメトリクスシステムから ANIMATION_DURATIONLAYOUT_MEASURE_DURATIONDRAW_DURATION を報告する。

各テストは 5 秒の収集ウィンドウで実行し、複数の構成をカバーして、Reanimated の最悪ケースと最良ケースの両方を見せるようにした。

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

Android(Moto G8 Plus)

Android は各ライブラリを最も公平に比較できる。どの実装も UI スレッド上で動くので、アニメーションエンジンごとに 1 フレームあたりどれだけ余分な処理が乗るかがそのまま数字に出る。小細工も抜け道もない。

ビルド構成でどれだけ変わるか(50 ビュー、平均ミリ秒)

Reanimated の性能を左右する最大の変数は、どのアニメーション API を選ぶかではなく、debug ビルドと release ビルドのどちらで計測するかだ。

比較チャート
構成EaseReanimated SVReanimated CSSRN Animated
Debug、FF なし6.1828.6227.419.38
Release、FF なし3.1411.8711.208.78
Release、FF 全部3.4310.579.068.82

赤線は 60fps における 16.67ms のフレームバジェットだ。debug ビルドでは、わずか 50 ビューで Reanimated SV と CSS がそろってバジェットを超え、実際にフレームを落としている。同じアニメーションが release ビルドなら 11ms で収まる。debug ビルドは嘘をつく。開発中にアニメーションの引っかかりに気づいたら、まず release ビルドで再現する。それから慌てるかどうかを決めればいい。

フィーチャーフラグは、レイアウトに影響しないプロパティの shadow tree commit を迂回することで、さらに 11〜19% の改善をもたらす。アプリによっては視覚的なバグを引き起こすため選択的に有効化するものだが、オーバーヘッドが気になるなら試す価値はある。

オーバーヘッドはビュー数に対してどう増えるか(Release、FF 全部、平均ミリ秒)

ビューの数が増えると、オーバーヘッドはどう変わるか。
ビュー数EaseReanimated SVReanimated CSSRN Animated
102.347.255.565.13
1003.9211.9810.269.93
5006.6339.9423.2921.85

💡 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 スレッドでは 1 フレームごとに本当に何も起きていない。代償として、Core Animation のアニメーションは実行中に JS から値を読んだり中断したりできない。ジェスチャー駆動のアニメーションは依然として Reanimated に頼ることになる。

1 フレームあたりの display link コールバック時間、ms(release ビルド)

毎フレームの表示チェーンにおけるコールバックの所要時間(単位は ms、release ビルド)
ViewsEaseReanimated SVReanimated SV (FF)Reanimated CSSReanimated CSS (FF)RN Animated
100.011.331.081.060.630.83
1000.013.723.332.712.483.32
5000.016.846.544.163.704.91

絶対値が Android より低いのは、ここでは UI スレッドのコールバック時間しか数えていないからだ。それでも結論は変わらない。iOS では何個のビューが動いていようと Ease は UI スレッドに一切のオーバーヘッドを加えず、他の実装は毎フレーム働いている。

React Native のアニメーションライブラリで毎フレームのコストが違う理由

shadow tree の税

Reanimated の worklet は毎フレーム新しい値を計算し、shadow tree を通じて prop の更新を 1 回コミットする。このコミットで Yoga のレイアウト、prop の diff、ビューの変更が走る。アニメーションしているのが transformopacity(レイアウトに一切影響しないプロパティ)なら、この作業はすべて無駄だ。blob を左に 3 ピクセル動かすために、レイアウト 1 回分のコストを払っている。Yoga はそんなことを知る必要がない。

feature flag(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 AnimateduseNativeDriver: true の組み合わせも毎フレームの JS スレッドを飛ばせるが、アニメーションは独立したネイティブアニメーションモジュールが駆動し、各アニメーションノードに簿記的なオーバーヘッドが乗る。ビュー数が中程度までなら悪くないが、アニメーションするビューが増えるにつれてスケールは Reanimated CSS に劣る。理由のひとつは、あの feature flag が有効にする shadow tree の最適化を欠いていることだ。

アニメーションライブラリの選択が本番アプリで本当に効いてくる場面

影響が最も大きいのは長時間動くアニメーションや遅いアニメーションだ。スケルトンローダー、ゆっくり流れる背景、アンビエントな UI エフェクトなど。5 秒間のアニメーションでは 1 フレーム落ちただけで目に見えるし、その間ほぼ常に別の処理(データ取得、再レンダリング、ユーザー操作)が走っている。リスト内のあらゆる要素も同じで、画面上に何百ものアニメーション項目が同時に存在することはざらにある。低スペック端末では毎フレームのわずかなオーバーヘッドがすぐ積み上がり、ユーザーのほうが先に気づく。

短い一度きりのトランジション(ボタン押下、toast、modal)ではこのオーバーヘッドは無視できる。どのライブラリを使ってもいい。

ひとつ断っておくと、Ease がカバーするのはこの特定のシナリオだけだ。ジェスチャー駆動のアニメーション(スクロール連動、ドラッグ、スワイプ)や、レイアウト属性(width、height、padding)を変えるアニメーションには、依然として Reanimated か RN Animated が要る。Ease は視覚属性に対する宣言的でトリガー駆動のアニメーションのために作られている。

React Native 0.85 と Shared Animation Backend

React Native 0.85 では実験的な Shared Animation Backend が導入された。Meta と Software Mansion がレンダラーに直接組み込んだ統一アニメーションエンジンだ。Reanimated の統合が入れば SYNCHRONOUSLY_UPDATE_UI_PROPS は不要になる。shadow tree の回避がデフォルトの経路になり、「デフォルトの Reanimated」と「最適化された Reanimated」の差は実質的になくなる。

とはいえアーキテクチャ上の違いは残る。Ease にはそもそもフレーム単位のアニメーションエンジンがない。バックエンドが速くなっても、Reanimated は毎フレーム値を計算して prop の更新を送り続ける。このオーバーヘッドは消えない。小さくなるだけだ。統合が入ったらベンチマークを更新する。

React Native アニメーションのベンチマークを自分で回す

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

ひとつ注意。release ビルドを使うこと。Debug モードでは Reanimated の数値が明らかに高く出る。数字が恐ろしく見えるなら、たいていこれが原因だ。

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

*react-native-easeApp & Flow が開発した。モントリオールの React Native エンジニアリングスタジオで、Expo が推薦するチームでもある。*

出典: Expo Blog← ホームへ戻る