OpenAIExpoApr 28, 2026, 1:30 PM

The real cost of React Native animations: benchmarking every approach

A condensed section focused on the key takeaways first.

Original Post

Quick Digest

Summary

A condensed section focused on the key takeaways first.

openaienmodel: gpt-5-mini-2025-08-07

The real cost of React Native animations: benchmarking every approach

Key Points

  • Ease yields ~0ms UI-thread cost on iOS
  • Reanimated costs explode in debug builds
  • Synchronous UI-prop flags cut Reanimated overhead 11–19%

Summary

A benchmark comparing four React Native animation approaches (react-native-ease, Reanimated shared values, Reanimated CSS, and RN Animated) measured per-frame UI-thread overhead on iOS (iPhone 15 Pro) and Android (Moto G8 Plus). Tests (Apr 2026) used Expo SDK 55, React Native 0.83, Reanimated 4.3.0, and react-native-ease 0.7.0. Main finding: on iOS, driving animations via platform APIs (Core Animation) yields effectively zero UI-thread cost; on Android all engines run on the UI thread so overhead matters and scales with view count. Reanimated performance is highly sensitive to debug vs release builds and benefits from synchronous UI-prop flags or the upcoming RN 0.85 Shared Animation Backend.

Key Points

  • Architecture matters: Ease (react-native-ease) hands animations to platform renderers (Core Animation on iOS, ObjectAnimator on Android). On iOS this results in ~0.01ms UI-thread cost because the OS render server drives frames outside the app.
  • Android parity: on Android every approach runs on the UI thread, so measured per-frame overhead is a direct cost. Ease is not free on Android; it still occupies the UI thread.
  • Debug vs Release: Reanimated shows dramatically higher overhead in debug builds. Reproduce performance in release builds first (debug builds can be misleading).
  • Scaling behavior: at 10–100 animated views most libraries average under 16.67ms, but at ~100 views Reanimated and RN Animated approach the frame budget with little headroom. At 500 views only Ease stayed under budget on the tested devices.
  • Shadow-tree tax & flags: Reanimated computes values and commits props through the shadow tree every frame, triggering Yoga/layout work even for visual-only props. ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPS and IOS_SYNCHRONOUSLY_UPDATE_UI_PROPS bypass shadow commits and reduced Reanimated per-frame cost by ~11–19% in tests.
  • Use cases: prefer platform-driven (Ease) animations for long-running, ambient, or non-gesture visual effects (drifting backgrounds, skeleton loaders). Use Reanimated or RN Animated for gesture-driven or layout-changing animations.

How it was measured

  • Measurement: CADisplayLink interception on iOS and Window.OnFrameMetricsAvailableListener on Android; 5-second collection windows per test.
  • Repo: benchmark is in the example app (example/src/demos/BenchmarkDemo.tsx). Run in release builds: yarn example ios --configuration Release or yarn example android --variant release.

Practical recommendations for engineers

  • Always test animations in release builds and on representative low-end devices.
  • For iOS ambient/long-running visual effects, prefer CA-backed libraries (react-native-ease) to remove per-frame UI-thread cost.
  • On Android, minimize concurrent animated views, enable Reanimated synchronous UI-prop flags to reduce overhead (test for visual regressions), and watch for RN 0.85 Shared Animation Backend to land in your stack.
  • Reserve Reanimated for gesture/interactive or layout animations where JS/worklets must drive values mid-flight.

Quick reference

  • Benchmarks: Apr 2026, Expo SDK 55, RN 0.83
  • Libraries: react-native-ease 0.7.0, Reanimated 4.3.0

Full Translation

Translations

A translation section that keeps the flow of the original article.

openaijamodel: gpt-5-mini-2025-08-07

React Nativeアニメーションの本当のコスト:あらゆるアプローチのベンチマーク

この寄稿はJanic Duplessisによるものです。彼はApp&FlowのHead of Consultingであり、長年のReact Nativeコントリビュータでもあります。

イメージしてみてください。ログイン画面の背後でゆっくりと漂う背景—製品を「作られた」ではなく「磨かれている」と感じさせるようなさりげない動き。単純に見えます。我々はそれをReanimatedで実装してリリースしました。しかし時々、フレームドロップが発生することがありました。わずかに違和感を与える程度ですが、一度気づくと気になるものです。

原因は、Reanimatedが毎フレームUIスレッド上で動作することにあります。フレーム中にアプリが重い処理(再レンダー、リストのスクロール、入力の更新など)を行うと、アニメーションに割ける予算が縮み、そのゆっくり動く背景のtranslateが“ゆっくりしたスタッター”になります。

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側のpropsとして記述され、ネイティブ側で駆動されるためフレームごとのJS関与はなし。
  • Reanimated(Shared Values):標準的なワークレットベースのアプローチ。C++ワークレットランタイム経由でUIスレッド上で値が駆動されるが、各フレームでシャドーツリーを通じてpropsを更新する。
  • Reanimated(CSS Animations):Reanimatedの新しいCSSアニメーションAPI。Easeのように宣言的だが、内部はReanimatedのアニメーションエンジンによる。
  • RN Animated:React Native組み込みのAnimated APIでuseNativeDriver: trueを使用。値はネイティブで駆動されるが、実装はプラットフォームごとに異なる。

また、Reanimatedを特定の静的フラグ(ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPSIOS_SYNCHRONOUSLY_UPDATE_UI_PROPS)を有効にした構成でもテストしました。これらは非レイアウト系props(transformやopacityなど)の更新時にシャドーツリーコミットをスキップできるため、意味のある最適化です。

注:RN 0.85は将来的にこれらフラグを不要にするShared Animation Backendを導入しました。Reanimated側の統合は進行中ですがまだリリースされていません。

ベンチマークの計測方法

ベンチマーク画面は、例アプリ内にN個のビューを同時にループでアニメートするスクリーンを作成しました(translateX、2s、linear、repeat)。フレームごとのオーバーヘッドはカスタムの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スレッド上で動くため、ここで見えているのは各アニメーションエンジンがフレームごとに追加する作業量の直接的な指標です。

  • 16.67msは60fps時のフレーム予算です。デバッグビルドではReanimated(Shared Values)とCSSの両方が50ビューでこの予算を超え、フレームを落としました。リリースビルドでは同じアニメーションが約11msに収まります。結論:デバッグビルドは実情を誤って示します。開発中にジャンクを見たら、慌てる前に必ずリリースビルドで再現してください。
  • フラグ(*_SYNCHRONOUSLY_UPDATE_UI_PROPS)はシャドーツリーコミットをバイパスすることでさらに11〜19%改善をもたらします。ただし一部のアプリで視覚的な不具合を引き起こす可能性があるためオプトインです。

どのようにビュー数でオーバーヘッドがスケールするか(リリース、全フラグ有効):

  • 10〜100ビューでは、すべてのアプローチが平均でフレーム予算内に収まりますが、ReanimatedとRN Animatedは100ビューで予算まで残り5ms程度しかなく、フレーム内の他処理にほとんど余裕がありません。
  • 500ビューはストレステストで現実的な目標ではありませんが、500ビュー時に予算内に残るのはEaseだけでした。Reanimated(SV)は36msに達し、フレーム予算の2倍以上です(これは最適化済み構成でも同様)。

iOS(iPhone 15 Pro)

iOSではアーキテクチャの違いが明確になります。AndroidではすべてのライブラリがUIスレッドを共有するため比較は公平ですが、iOSではEaseが“合法的にズル”できます。Core Animationは別プロセスのOSレンダーサーバで実行され、アプリ側ではフレームごとの処理が発生しません。一度CAAnimationを登録すればシステムが駆動し、アプリのスレッドは完全に解放されます。

  • そのためEaseはどのビュー数でもUIスレッド上のオーバーヘッドがおおよそ~0.01msと報告されます:実際にフレームごとにUIスレッドで何もしていないからです。
  • 他のアプローチはビュー数に関係なく毎フレーム何らかの作業を続けます。

(注:計測上の絶対値はAndroidより低めに出ます。iOSの計測はUIスレッドのコールバック時間のみを捕捉しているためです。)

なぜReact Nativeのアニメーションライブラリはフレームごとのコストで差が出るのか

シャドーツリー税

毎フレーム、Reanimatedのワークレットは新しい値を計算し、プロップ更新をシャドーツリーを通じてコミットします。そのコミットではYogaレイアウト、propのdiff、ビューのミューテーションが走ります。transformやopacityのようにレイアウトに影響しないプロパティをアニメートしているとき、この作業の多くは無駄です。幅を3ピクセル動かすためにフルレイアウトパスの代償を払っているようなものです。

ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPS / 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

useNativeDriver: trueを使ったRN AnimatedもフレームごとのJSスレッドはスキップしますが、ネイティブ側に独自のアニメーションモジュールがあり、各アニメーションノードごとのブックキーピングオーバーヘッドを持ちます。低〜中ビュー数では十分に耐えますが、アニメートするビュー数が増えるとスケールが悪くなります。部分的には、前述のシャドーツリー最適化(フラグ)がないことが原因です。

本番アプリでアニメーションライブラリの選択が重要になるとき

  • 長時間継続する、または遅いアニメーション(スケルトンローダー、漂う背景、常時表示されるUIエフェクト)では重要です。5秒のアニメーションでの単一フレームドロップは目立ちますし、データフェッチや再レンダー、ユーザー操作が同時に起きることが多いです。
  • リスト内のアニメーション(数百のアニメートされたアイテムが一度に存在する)でも重要です。低スペック端末では小さなフレームごとのオーバーヘッドが急速に累積し、ユーザーが先に気づきます。
  • 一方で、短時間のワンショットトランジション(ボタン押下、トースト、モーダル)はオーバーヘッドが無視できるレベルで、どのライブラリでも問題ありません。

補足: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は不要になり、シャドーツリーのバイパスがデフォルトパスになります。これにより「デフォルトのReanimated」と「最適化したReanimated」の差は実質的に無くなります。

ただしアーキテクチャ上の違いは残ります。Easeはそもそもフレームごとのアニメーションエンジンを持ちません。より高速なバックエンドが登場しても、Reanimatedは依然として毎フレーム値を計算してプロップ更新を押し出します。そのオーバーヘッドは消えず、ただ小さくなるに過ぎません。統合が出たらベンチマークを更新します。

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

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

  1. 注意点:必ずリリースビルドを使ってください。デバッグモードはReanimatedの数値を大幅に膨らませます。もし数字が異常に見えるなら、それが理由の可能性が高いです。

  2. 実行コマンド例:

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

react-native-easeは、モントリオール拠点のReact NativeエンジニアリングスタジオであるApp & Flowが構築したライブラリで、Expoの推奨を受けています。