Skip to content

Commit

Permalink
Animated: Lazily Allocate AnimatedNode Instances (#46317)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #46317

Changes `AnimatedProps` to avoid allocating `AnimatedStyle` (and `AnimatedTransform`, `AnimatedObject`) unless necessary.

This not only reduces memory and traversal overhead, but it enables us to implement allowlist strategies to prune unnecessary traversals.

Changelog:
[General][Changed] - Animated now omits `style` if the supplied value is null, undefined, or not an object. Previously, it would emit an empty `style` object.
[General][Changed] - Animated now resolves `style` to the original prop value if it contains no `AnimatedNode` instances. Previously, it would resolve to a flattened style object.

Reviewed By: javache

Differential Revision: D62117423

fbshipit-source-id: 34b0c9940be5b6f5d94467993a5344406cc56f93
  • Loading branch information
yungsters authored and facebook-github-bot committed Sep 10, 2024
1 parent 9e6c4fd commit ca234ba
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/

import AnimatedProps from '../nodes/AnimatedProps';

describe('AnimatedProps', () => {
function getValue(inputProps: {[string]: mixed}) {
const animatedProps = new AnimatedProps(inputProps, jest.fn());
return animatedProps.__getValue();
}

it('returns original `style` if it has no nodes', () => {
const style = {color: 'red'};
expect(getValue({style}).style).toBe(style);
});

it('returns original `style` for invalid style values', () => {
const values = [undefined, null, function () {}, true, 123, 'foo'];
for (const value of values) {
expect(getValue({style: value})).toEqual({style: value});
}
});
});
12 changes: 8 additions & 4 deletions packages/react-native/Libraries/Animated/nodes/AnimatedProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,14 @@ function createAnimatedProps(
const value = inputProps[key];

if (key === 'style') {
const node = new AnimatedStyle(value);
nodeKeys.push(key);
nodes.push(node);
props[key] = node;
const node = AnimatedStyle.from(value);
if (node == null) {
props[key] = value;
} else {
nodeKeys.push(key);
nodes.push(node);
props[key] = node;
}
} else if (value instanceof AnimatedNode) {
const node = value;
nodeKeys.push(key);
Expand Down
33 changes: 25 additions & 8 deletions packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function createAnimatedStyle(
const node = ReactNativeFeatureFlags.shouldUseAnimatedObjectForTransform()
? AnimatedObject.from(value)
: // $FlowFixMe[incompatible-call] - `value` is mixed.
new AnimatedTransform(value);
AnimatedTransform.from(value);
if (node == null) {
if (keepUnanimatedValues) {
style[key] = value;
Expand Down Expand Up @@ -77,19 +77,36 @@ export default class AnimatedStyle extends AnimatedWithChildren {
_inputStyle: any;
_style: {[string]: any};

constructor(inputStyle: any) {
super();
this._inputStyle = inputStyle;
/**
* Creates an `AnimatedStyle` if `value` contains `AnimatedNode` instances.
* Otherwise, returns `null`.
*/
static from(inputStyle: any): ?AnimatedStyle {
const flatStyle = flattenStyle(inputStyle);
if (flatStyle == null) {
return null;
}
const [nodeKeys, nodes, style] = createAnimatedStyle(
// NOTE: This null check should not be necessary, but the types are not
// strong nor enforced as of this writing. This check should be hoisted
// to instantiation sites.
flattenStyle(inputStyle) ?? {},
flatStyle,
Platform.OS !== 'web',
);
if (nodes.length === 0) {
return null;
}
return new AnimatedStyle(nodeKeys, nodes, style, inputStyle);
}

constructor(
nodeKeys: $ReadOnlyArray<string>,
nodes: $ReadOnlyArray<AnimatedNode>,
style: {[string]: any},
inputStyle: any,
) {
super();
this.#nodeKeys = nodeKeys;
this.#nodes = nodes;
this._style = style;
this._inputStyle = inputStyle;
}

__getValue(): Object | Array<Object> {
Expand Down
65 changes: 43 additions & 22 deletions packages/react-native/Libraries/Animated/nodes/AnimatedTransform.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,37 +26,58 @@ type Transform<T = AnimatedNode> = {
| {[string]: number | string | T},
};

function flatAnimatedNodes(
transforms: $ReadOnlyArray<Transform<>>,
): Array<AnimatedNode> {
const nodes = [];
for (let ii = 0, length = transforms.length; ii < length; ii++) {
const transform = transforms[ii];
// There should be exactly one property in `transform`.
for (const key in transform) {
const value = transform[key];
if (value instanceof AnimatedNode) {
nodes.push(value);
}
}
}
return nodes;
}

export default class AnimatedTransform extends AnimatedWithChildren {
// NOTE: For potentially historical reasons, some operations only operate on
// the first level of AnimatedNode instances. This optimizes that bevavior.
#shallowNodes: $ReadOnlyArray<AnimatedNode>;
#nodes: $ReadOnlyArray<AnimatedNode>;

_transforms: $ReadOnlyArray<Transform<>>;

constructor(transforms: $ReadOnlyArray<Transform<>>) {
/**
* Creates an `AnimatedTransform` if `transforms` contains `AnimatedNode`
* instances. Otherwise, returns `null`.
*/
static from(transforms: $ReadOnlyArray<Transform<>>): ?AnimatedTransform {
const nodes = flatAnimatedNodes(
// NOTE: This check should not be necessary, but the types are not
// enforced as of this writing. This check should be hoisted to
// instantiation sites.
Array.isArray(transforms) ? transforms : [],
);
if (nodes.length === 0) {
return null;
}
return new AnimatedTransform(nodes, transforms);
}

constructor(
nodes: $ReadOnlyArray<AnimatedNode>,
transforms: $ReadOnlyArray<Transform<>>,
) {
super();
this.#nodes = nodes;
this._transforms = transforms;

const shallowNodes = [];
// NOTE: This check should not be necessary, but the types are not enforced
// as of this writing. This check should be hoisted to instantiation sites.
if (Array.isArray(transforms)) {
for (let ii = 0, length = transforms.length; ii < length; ii++) {
const transform = transforms[ii];
// There should be exactly one property in `transform`.
for (const key in transform) {
const value = transform[key];
if (value instanceof AnimatedNode) {
shallowNodes.push(value);
}
}
}
}
this.#shallowNodes = shallowNodes;
}

__makeNative(platformConfig: ?PlatformConfig) {
const nodes = this.#shallowNodes;
const nodes = this.#nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__makeNative(platformConfig);
Expand All @@ -77,15 +98,15 @@ export default class AnimatedTransform extends AnimatedWithChildren {
}

__attach(): void {
const nodes = this.#shallowNodes;
const nodes = this.#nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__addChild(this);
}
}

__detach(): void {
const nodes = this.#shallowNodes;
const nodes = this.#nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__removeChild(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,18 @@ exports[`LogBoxInspectorSourceMapStatus should render for failed 1`] = `
}
}
style={
Object {
"height": 14,
"marginEnd": 4,
"tintColor": "rgba(243, 83, 105, 1)",
"width": 16,
}
Array [
Object {
"height": 14,
"marginEnd": 4,
"tintColor": "rgba(255, 255, 255, 0.4)",
"width": 16,
},
Object {
"tintColor": "rgba(243, 83, 105, 1)",
},
null,
]
}
/>
<Text
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -970,7 +970,13 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A
"declare export default class AnimatedStyle extends AnimatedWithChildren {
_inputStyle: any;
_style: { [string]: any };
constructor(inputStyle: any): void;
static from(inputStyle: any): ?AnimatedStyle;
constructor(
nodeKeys: $ReadOnlyArray<string>,
nodes: $ReadOnlyArray<AnimatedNode>,
style: { [string]: any },
inputStyle: any
): void;
__getValue(): Object | Array<Object>;
__getAnimatedValue(): Object;
__attach(): void;
Expand Down Expand Up @@ -1034,7 +1040,11 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A
};
declare export default class AnimatedTransform extends AnimatedWithChildren {
_transforms: $ReadOnlyArray<Transform<>>;
constructor(transforms: $ReadOnlyArray<Transform<>>): void;
static from(transforms: $ReadOnlyArray<Transform<>>): ?AnimatedTransform;
constructor(
nodes: $ReadOnlyArray<AnimatedNode>,
transforms: $ReadOnlyArray<Transform<>>
): void;
__makeNative(platformConfig: ?PlatformConfig): void;
__getValue(): $ReadOnlyArray<Transform<any>>;
__getAnimatedValue(): $ReadOnlyArray<Transform<any>>;
Expand Down

0 comments on commit ca234ba

Please sign in to comment.