From 62794774b91c2b9915f21c333165179e2bbb6549 Mon Sep 17 00:00:00 2001 From: Mark Verlingieri Date: Tue, 14 Apr 2026 11:16:01 -0700 Subject: [PATCH] Process event-driven animations synchronously on every event (#56439) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: The C++ NativeAnimatedNodesManager only processes the first scroll event in a gesture synchronously via trigger()/onRender(). Subsequent events just store the updated value and defer graph traversal + prop commit to the next choreographer frame, introducing ~16ms (1 frame) latency. This differs from the Java NativeAnimatedNodesManager which calls updateNodes() → updateView() → synchronouslyUpdateViewOnUIThread() for every event with no gating. The issue was the `!isEventAnimationInProgress_` guard at line 502, which prevented trigger()/onRender() from running on the 2nd+ events. `isEventAnimationInProgress_` stays true for the entire scroll gesture (only resets when a choreographer frame produces zero changes), so every scroll event after the first was deferred. This change moves the trigger()/onRender() call outside the `!isEventAnimationInProgress_` gate so every scroll event gets immediate synchronous processing, matching the Java implementation. The startRenderCallbackIfNeeded + isEventAnimationInProgress_ setup still only happens once (first event). ## Changelog: [General] [Fixed] - Fix 1-frame latency in C++ NativeAnimatedNodesManager for event-driven animations by processing the animation graph synchronously on every scroll event, matching the Java implementation behavior Reviewed By: zeyap Differential Revision: D100719854 --- .../animated/NativeAnimatedNodesManager.cpp | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp index 8cac7589ca0c..991ec92327a1 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp @@ -499,15 +499,23 @@ void NativeAnimatedNodesManager::handleAnimatedEvent( } } - if (foundAtLeastOneDriver && !isEventAnimationInProgress_) { - // There is an animation driver handling this event and - // event driven animation has not been started yet. - isEventAnimationInProgress_ = true; - // Some platforms (e.g. iOS) have UI tick listener disable - // when there are no active animations. Calling - // `startRenderCallbackIfNeeded` will call platform specific code to - // register UI tick listener. - startRenderCallbackIfNeeded(false); + if (foundAtLeastOneDriver) { + // Process event-driven animation updates synchronously, matching the + // behavior of the Java NativeAnimatedNodesManager which calls + // updateNodes() for every event. Without this, only the first event + // in a scroll sequence is processed synchronously — subsequent events + // just store the updated value and defer graph traversal + prop commit + // to the next choreographer frame, introducing 1-frame latency. + if (!isEventAnimationInProgress_) { + // There is an animation driver handling this event and + // event driven animation has not been started yet. + isEventAnimationInProgress_ = true; + // Some platforms (e.g. iOS) have UI tick listener disable + // when there are no active animations. Calling + // `startRenderCallbackIfNeeded` will call platform specific code to + // register UI tick listener. + startRenderCallbackIfNeeded(false); + } // Calling startOnRenderCallback_ will register a UI tick listener. // The UI ticker listener will not be called until the next frame. // That's why, in case this is called from the UI thread, we need to