Skip to content

Commit 8a09693

Browse files
shubh73claude
andcommitted
Avoid allocations in Animated string interpolation
The string and color branches of `createStringInterpolation` allocated two arrays and a closure per call (`interpolations.map`, `components.map`, `join`). Build the string with a plain loop and compute rgba channels as locals instead. Output is unchanged. Adds `AnimatedInterpolation-benchmark-itest.js` and test cases for strings with several numeric components. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent d7ff82e commit 8a09693

3 files changed

Lines changed: 100 additions & 11 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
12+
13+
import type {InterpolationConfigType} from '../nodes/AnimatedInterpolation';
14+
15+
import AnimatedInterpolation from '../nodes/AnimatedInterpolation';
16+
import * as Fantom from '@react-native/fantom';
17+
18+
const CALLS = 1_000_000;
19+
20+
function createInterpolation(
21+
config: InterpolationConfigType<string>,
22+
): number => string {
23+
let parentValue = 0;
24+
const interpolation = new AnimatedInterpolation(
25+
// $FlowFixMe[incompatible-type]
26+
{__getValue: () => parentValue},
27+
config,
28+
);
29+
return input => {
30+
parentValue = input;
31+
return interpolation.__getValue();
32+
};
33+
}
34+
35+
function run(interpolation: number => string) {
36+
for (let i = 0; i < CALLS; i++) {
37+
interpolation(i / CALLS);
38+
}
39+
}
40+
41+
const rotate = createInterpolation({
42+
inputRange: [0, 1],
43+
outputRange: ['0deg', '360deg'],
44+
});
45+
const shadow = createInterpolation({
46+
inputRange: [0, 1],
47+
outputRange: ['0px 0px 0px', '-10.5px 20px 4px'],
48+
});
49+
const color = createInterpolation({
50+
inputRange: [0, 1],
51+
outputRange: ['#FF9500', 'rgba(50, 150, 250, 0.4)'],
52+
});
53+
54+
Fantom.unstable_benchmark
55+
.suite('AnimatedInterpolation', {minIterations: 10})
56+
.test(`string output, 1 number (${CALLS} calls)`, () => {
57+
run(rotate);
58+
})
59+
.test(`string output, 3 numbers (${CALLS} calls)`, () => {
60+
run(shadow);
61+
})
62+
.test(`color output (${CALLS} calls)`, () => {
63+
run(color);
64+
});

‎packages/react-native/Libraries/Animated/__tests__/Interpolation-itest.js‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,28 @@ describe('Interpolation', () => {
311311
expect(interpolation(1)).toBe('100deg');
312312
});
313313

314+
it('should work with multiple numeric components in string ranges', () => {
315+
const interpolation = createInterpolation({
316+
inputRange: [0, 1],
317+
outputRange: ['0px -4.5px 10%', '10px 5.5px 0%'],
318+
});
319+
320+
expect(interpolation(0)).toBe('0px -4.5px 10%');
321+
expect(interpolation(0.5)).toBe('5px 0.5px 5%');
322+
expect(interpolation(1)).toBe('10px 5.5px 0%');
323+
});
324+
325+
it('should keep the non-numeric prefix and suffix of string ranges', () => {
326+
const interpolation = createInterpolation({
327+
inputRange: [0, 1],
328+
outputRange: ['translate(0px, -10.5px)', 'translate(20px, 10px)'],
329+
});
330+
331+
expect(interpolation(0)).toBe('translate(0px, -10.5px)');
332+
expect(interpolation(0.5)).toBe('translate(10px, -0.25px)');
333+
expect(interpolation(1)).toBe('translate(20px, 10px)');
334+
});
335+
314336
it('should crash when chaining an interpolation that returns a string', () => {
315337
const interpolation = createInterpolation({
316338
inputRange: [0, 1],

‎packages/react-native/Libraries/Animated/nodes/AnimatedInterpolation.js‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -277,22 +277,25 @@ function createStringInterpolation(
277277
}),
278278
);
279279
if (!isColor) {
280+
const components = outputRange[0].components;
280281
return input => {
281-
const values = interpolations.map(interpolation => interpolation(input));
282+
let result = '';
282283
let i = 0;
283-
return outputRange[0].components
284-
.map(c => (typeof c === 'number' ? values[i++] : c))
285-
.join('');
284+
for (let j = 0; j < components.length; j++) {
285+
const c = components[j];
286+
result += typeof c === 'number' ? interpolations[i++](input) : c;
287+
}
288+
return result;
286289
};
287290
} else {
288291
return input => {
289-
const result = interpolations.map((interpolation, i) => {
290-
const value = interpolation(input);
291-
// rgba requires that the r,g,b are integers.... so we want to round them, but we *dont* want to
292-
// round the opacity (4th column).
293-
return i < 3 ? Math.round(value) : Math.round(value * 1000) / 1000;
294-
});
295-
return `rgba(${result[0]}, ${result[1]}, ${result[2]}, ${result[3]})`;
292+
// rgba requires that the r,g,b are integers.... so we want to round them, but we *dont* want to
293+
// round the opacity (4th column).
294+
const r = Math.round(interpolations[0](input));
295+
const g = Math.round(interpolations[1](input));
296+
const b = Math.round(interpolations[2](input));
297+
const a = Math.round(interpolations[3](input) * 1000) / 1000;
298+
return `rgba(${r}, ${g}, ${b}, ${a})`;
296299
};
297300
}
298301
}

0 commit comments

Comments
 (0)