Skip to content

Animate filter without forcing a Fabric commit every frame - #58071

Open
dennytosp wants to merge 1 commit into
react:mainfrom
dennytosp:fix/animated-filter-direct-manipulation
Open

Animate filter without forcing a Fabric commit every frame#58071
dennytosp wants to merge 1 commit into
react:mainfrom
dennytosp:fix/animated-filter-direct-manipulation

Conversation

@dennytosp

@dennytosp dennytosp commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Summary:

NativeAnimatedAllowlist.h carries this instruction:

/**
 * Direct manipulation eligible styles allowed by the NativeAnimated JS
 * implementation. Keep in sync with
 * packages/react-native/Libraries/Animated/NativeAnimatedAllowlist.js
 */

It has drifted by one entry. SUPPORTED_STYLES in the JS file lists filter between opacity and transform; the C++ set goes straight from "opacity" to "transform". Every other member of the JS list's non-layout half is present, so this is a missed sync rather than a deliberate exclusion.

The set is not advisory — it is what separates layout props from paint props:

// StyleAnimatedNode.cpp
bool isLayoutPropsUpdated(const folly::dynamic& props) {
  for (const auto& styleNodeProp : props.items()) {
    if (getDirectManipulationAllowlist().count(styleNodeProp.first.asString()) == 0u) {
      return true;   // absent => treated as a layout prop
    }
  }
  return false;
}

and that verdict picks the transport:

// NativeAnimatedNodesManager.cpp
auto& current = layoutStyleUpdated
    ? updateViewPropsForBackend_[viewTag]        // Fabric commit
    : updateViewPropsDirectForBackend_[viewTag]; // direct manipulation

So animating filter runs a shadow-tree commit on every frame instead of taking the direct-manipulation path, for a property that cannot affect layout — filter lives in BaseViewProps (std::vector<FilterFunction> filter{}), not in YogaStylableProps, and nothing in react/renderer/animated/ treats it specially.

Note the C++ set is deliberately not a mirror of the whole JS SUPPORTED_STYLES: the entries the JS file adds under useSharedAnimatedBackend() (width, height, margin, padding, flex, gap, …) are genuine layout props and must stay out so they keep going through Fabric. Only the non-layout half has to match, and filter belongs to it.

Changelog:

[GENERAL] [FIXED] - Animating filter no longer forces a Fabric commit on every frame

Test Plan:

Added directManipulationAllowlistCoversNonLayoutStyles to AnimatedNodeTests. It asserts the allowlist contains every non-layout style the JS file supports, and — so the test cannot be satisfied by simply widening the set — that the layout styles are still absent.

The allowlist header is dependency-free, so the invariant can also be checked directly:

$ c++ -std=c++20 -Wall -Wextra -I packages/react-native/ReactCommon allowcheck.cpp -o allowcheck

# before
MISSING from C++ allowlist: filter
checked 34 JS non-layout styles, missing=1     (exit 1)

# after
checked 34 JS non-layout styles, missing=0     (exit 0)
$ yarn jest packages/react-native/Libraries/Animated
Test Suites: 3 passed, 3 total
Tests:       66 passed, 66 total

$ node ./scripts/clang-format.js <both changed files>
(no changes)

getDirectManipulationAllowlist appears in the C++ API snapshots, but only by signature — this changes the contents of the static set, not the declaration, so scripts/cxx-api is unaffected.

The gtest itself was neither executed locally nor built by the public CI. react/renderer/animated/tests is excluded from the iOS build (React-Fabric.podspec: ss.exclude_files = "react/renderer/animated/tests") and is not in the Android CMake glob (react/renderer/animated/CMakeLists.txt globs *.cpp drivers/*.cpp event_drivers/*.cpp internal/*.cpp nodes/*.cpp), so it builds only in the internal build reached at import time. What is verified here: the test body compiles clean against the real header under -std=c++20 -Wall -Wextra, and the standalone parity check above exercises the same assertions and fails without the one-line change.

The C++ direct-manipulation allowlist is missing 'filter', which
NativeAnimatedAllowlist.js lists as an animatable style. Anything absent
from that set is classified as a layout update by StyleAnimatedNode, so
filter animations are committed through the shadow tree each frame
instead of taking the direct-manipulation path.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 22, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Aug 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant