Skip to content

Commit 2d237d0

Browse files
rubennortefacebook-github-bot
authored andcommitted
Expand structuredClone compatibility and benchmarks (#58735)
Summary: Compare React Native structuredClone with ungap/structured-clone (**used by Expo**) and port the applicable upstream coverage. Add support for ArrayBuffer, DataView, typed arrays, boxed BigInt values, Error subclasses, deeply cloned causes, and safe __proto__ data properties. Serialization-only and JSON/lossy tests remain out of scope because React Native exposes a single-step cloning API. Add ungap/structured-clone as a devDependency and benchmark each existing Fantom scenario against it. Before — React Native implementation before this change: ### structuredClone (mode 🚀) ### | (index) | Task name | Latency avg (ns) | Latency med (ns) | Throughput avg (ops/s) | Throughput med (ops/s) | Samples | | ------- | ------------------------ | ----------------- | ---------------- | ---------------------- | ---------------------- | ------- | | 0 | 'clone a string' | '338.62 ± 0.05%' | '330.00 ± 9.00' | '3014547 ± 0.05%' | '3030303 ± 84962' | 2953184 | | 1 | 'clone a basic array' | '1587.6 ± 10.36%' | '1353.0 ± 19.00' | '734465 ± 0.42%' | '739098 ± 10235' | 638616 | | 2 | 'clone a basic object' | '1344.2 ± 8.81%' | '1172.0 ± 20.00' | '828917 ± 0.33%' | '853242 ± 14813' | 743963 | | 3 | 'clone a complex object' | '3370.6 ± 0.60%' | '3255.0 ± 40.00' | '304183 ± 0.02%' | '307220 ± 3822' | 296680 | After — React Native compared with ungap/structured-clone: ### structuredClone (mode 🚀) ### | (index) | Task name | Latency avg (ns) | Latency med (ns) | Throughput avg (ops/s) | Throughput med (ops/s) | Samples | | ------- | -------------------------------------- | ----------------- | ----------------- | ---------------------- | ---------------------- | ------- | | 0 | 'React Native: clone a string' | '348.44 ± 0.08%' | '331.00 ± 1.00' | '2927181 ± 0.04%' | '3021148 ± 9155' | 2869970 | | 1 | 'ungap: clone a string' | '2037.4 ± 11.17%' | '1592.0 ± 50.00' | '617833 ± 0.09%' | '628141 ± 19947' | 490832 | | 2 | 'React Native: clone a basic array' | '1460.2 ± 8.39%' | '1282.0 ± 20.00' | '775137 ± 0.42%' | '780031 ± 12362' | 684824 | | 3 | 'ungap: clone a basic array' | '5112.8 ± 0.81%' | '4718.0 ± 111.00' | '205586 ± 0.08%' | '211954 ± 5107' | 195589 | | 4 | 'React Native: clone a basic object' | '1373.8 ± 8.58%' | '1152.0 ± 20.00' | '842837 ± 0.46%' | '868056 ± 15337' | 727910 | | 5 | 'ungap: clone a basic object' | '8853.2 ± 0.76%' | '8253.0 ± 141.00' | '117562 ± 0.05%' | '121168 ± 2106' | 112954 | | 6 | 'React Native: clone a complex object' | '3539.4 ± 0.55%' | '3456.0 ± 50.00' | '288368 ± 0.02%' | '289352 ± 4248' | 282537 | | 7 | 'ungap: clone a complex object' | '21498 ± 0.72%' | '20080 ± 301.00' | '48208 ± 0.09%' | '49801 ± 755' | 46516 | The benchmark shows no material regression in the existing implementation and React Native remains faster than ungap/structured-clone across all scenarios. Changelog: [General][Added] - Add ArrayBuffer, DataView, typed array, boxed BigInt, and Error subclass support to structuredClone Differential Revision: D122156629
1 parent d194d8b commit 2d237d0

6 files changed

Lines changed: 354 additions & 50 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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+
declare module '@ungap/structured-clone' {
12+
declare export default function structuredClone<T>(value: T): T;
13+
}

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"@tsconfig/node22": "22.0.2",
7575
"@types/react": "^19.1.0",
7676
"@typescript-eslint/parser": "^8.59.2",
77+
"@ungap/structured-clone": "^1.4.0",
7778
"ansi-regex": "^5.0.0",
7879
"ansi-styles": "^4.2.1",
7980
"babel-plugin-minify-dead-code-elimination": "^0.5.2",

‎packages/react-native/src/private/webapis/structuredClone/__tests__/structuredClone-benchmark-itest.js‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
1212

1313
import structuredClone from '../structuredClone';
1414
import * as Fantom from '@react-native/fantom';
15+
import ungapStructuredClone from '@ungap/structured-clone';
1516

1617
const basicArray = [1, 2, 3];
1718
const basicObject = {a: 1, b: 2, c: 3};
@@ -29,15 +30,27 @@ const complexObject = {
2930

3031
Fantom.unstable_benchmark
3132
.suite('structuredClone')
32-
.test('clone a string', () => {
33+
.test('React Native: clone a string', () => {
3334
structuredClone('hello world');
3435
})
35-
.test('clone a basic array', () => {
36+
.test('@ungap: clone a string', () => {
37+
ungapStructuredClone('hello world');
38+
})
39+
.test('React Native: clone a basic array', () => {
3640
structuredClone(basicArray);
3741
})
38-
.test('clone a basic object', () => {
42+
.test('@ungap: clone a basic array', () => {
43+
ungapStructuredClone(basicArray);
44+
})
45+
.test('React Native: clone a basic object', () => {
3946
structuredClone(basicObject);
4047
})
41-
.test('clone a complex object', () => {
48+
.test('@ungap: clone a basic object', () => {
49+
ungapStructuredClone(basicObject);
50+
})
51+
.test('React Native: clone a complex object', () => {
4252
structuredClone(complexObject);
53+
})
54+
.test('@ungap: clone a complex object', () => {
55+
ungapStructuredClone(complexObject);
4356
});

‎packages/react-native/src/private/webapis/structuredClone/__tests__/structuredClone-itest.js‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ describe('structuredClone', () => {
6767

6868
expect(structuredClone(0)).toBe(0);
6969
expect(structuredClone(1)).toBe(1);
70+
expect(Object.is(structuredClone(-0), -0)).toBe(true);
7071

7172
expect(structuredClone(0n)).toBe(0n);
7273
expect(structuredClone(1n)).toBe(1n);
@@ -99,6 +100,12 @@ describe('structuredClone', () => {
99100
expect(booleanClone).not.toBe(booleanValue);
100101
expect(booleanClone).toBeInstanceOf(Boolean);
101102
expect(booleanClone.valueOf()).toBe(true);
103+
104+
const bigintValue = Object(1n);
105+
const bigintClone = structuredClone(bigintValue);
106+
expect(bigintClone).not.toBe(bigintValue);
107+
expect(bigintClone).toBeInstanceOf(BigInt);
108+
expect(bigintClone.valueOf()).toBe(1n);
102109
});
103110

104111
it('throws with symbols, functions, WeakMap, WeakSet, Promise', () => {
@@ -223,6 +230,58 @@ describe('structuredClone', () => {
223230
expect(clone).toEqual(value);
224231
});
225232

233+
it('clones invalid dates', () => {
234+
const value = new Date(NaN);
235+
const clone = structuredClone(value);
236+
expect(clone).not.toBe(value);
237+
expect(clone).toBeInstanceOf(Date);
238+
expect(Number.isNaN(clone.getTime())).toBe(true);
239+
});
240+
241+
it('clones ArrayBuffer', () => {
242+
const value = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]).buffer;
243+
const clone = structuredClone(value);
244+
expect(clone).not.toBe(value);
245+
expect(clone).toBeInstanceOf(ArrayBuffer);
246+
expect([...new Uint8Array(clone)]).toEqual([0, 1, 2, 3, 4, 5, 6, 7]);
247+
});
248+
249+
it('clones DataView', () => {
250+
const buffer = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]).buffer;
251+
const value = new DataView(buffer);
252+
const clone = structuredClone(value);
253+
expect(clone).not.toBe(value);
254+
expect(clone).toBeInstanceOf(DataView);
255+
expect([...new Uint8Array(clone.buffer)]).toEqual([0, 1, 2, 3, 4, 5, 6, 7]);
256+
});
257+
258+
it('clones typed arrays', () => {
259+
const value = new Uint32Array([1, 2, 3]);
260+
const clone = structuredClone(value);
261+
expect(clone).not.toBe(value);
262+
expect(clone).toBeInstanceOf(Uint32Array);
263+
expect([...clone]).toEqual([1, 2, 3]);
264+
});
265+
266+
it('preserves shared buffers between views', () => {
267+
const buffer = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]).buffer;
268+
const value = {
269+
typedArray: new Uint16Array(buffer, 2, 2),
270+
dataView: new DataView(buffer, 1, 6),
271+
buffer,
272+
};
273+
274+
const clone = structuredClone(value);
275+
expect(clone.buffer).not.toBe(buffer);
276+
expect(clone.dataView.buffer).toBe(clone.buffer);
277+
expect(clone.typedArray.buffer).toBe(clone.buffer);
278+
expect(clone.dataView.byteOffset).toBe(value.dataView.byteOffset);
279+
expect(clone.dataView.byteLength).toBe(value.dataView.byteLength);
280+
expect(clone.typedArray.byteOffset).toBe(value.typedArray.byteOffset);
281+
expect(clone.typedArray.length).toBe(value.typedArray.length);
282+
expect([...clone.typedArray]).toEqual([...value.typedArray]);
283+
});
284+
226285
it('clones errors', () => {
227286
const cause = new Error('cause message');
228287
const value = new Error('error message', {cause});
@@ -261,6 +320,53 @@ describe('structuredClone', () => {
261320
expect(structuredClone(value).name).toBe('Error');
262321
});
263322

323+
it('preserves error subclasses', () => {
324+
expect(structuredClone(new Error('boom'))).toBeInstanceOf(Error);
325+
expect(structuredClone(new EvalError('boom'))).toBeInstanceOf(EvalError);
326+
expect(structuredClone(new RangeError('boom'))).toBeInstanceOf(RangeError);
327+
expect(structuredClone(new ReferenceError('boom'))).toBeInstanceOf(
328+
ReferenceError,
329+
);
330+
expect(structuredClone(new SyntaxError('boom'))).toBeInstanceOf(
331+
SyntaxError,
332+
);
333+
expect(structuredClone(new TypeError('boom'))).toBeInstanceOf(TypeError);
334+
expect(structuredClone(new URIError('boom'))).toBeInstanceOf(URIError);
335+
});
336+
337+
it('clones aggregate errors', () => {
338+
const innerError = new TypeError('inner');
339+
const value = new AggregateError([innerError, {foo: 'bar'}], 'outer');
340+
const clone = structuredClone(value);
341+
const clonedErrors = Array.from(clone.errors);
342+
const clonedInnerError = ensureInstance(clonedErrors[0], TypeError);
343+
344+
expect(clone).not.toBe(value);
345+
expect(clone).toBeInstanceOf(AggregateError);
346+
expect(clone.message).toBe(value.message);
347+
expect(clone.stack).toBe(value.stack);
348+
expect(clone.errors).not.toBe(value.errors);
349+
expect(clonedInnerError).not.toBe(innerError);
350+
expect(clonedInnerError.message).toBe(innerError.message);
351+
expect(clonedErrors[1]).toEqual({foo: 'bar'});
352+
});
353+
354+
it('clones __proto__ as an own property', () => {
355+
const value = JSON.parse('{"__proto__":{"foo":"bar"}}');
356+
const clone = structuredClone(value);
357+
const clonedValue = nullthrows(
358+
Object.getOwnPropertyDescriptor(clone, '__proto__'),
359+
).value;
360+
const originalValue = nullthrows(
361+
Object.getOwnPropertyDescriptor(value, '__proto__'),
362+
).value;
363+
364+
expect(clone.foo).toBeUndefined();
365+
expect(Object.hasOwn(clone, '__proto__')).toBe(true);
366+
expect(clonedValue).not.toBe(originalValue);
367+
expect(clonedValue).toEqual({foo: 'bar'});
368+
});
369+
264370
it('clones values deeply', () => {
265371
const value = {
266372
obj: {

0 commit comments

Comments
 (0)