From caaa5c93fb633ae734d9fc8654bb95599c0c8cda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Tue, 13 May 2025 14:29:01 -0700 Subject: [PATCH] Optimize structuredClone (#51250) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/51250 Changelog: [internal] Optimizes the implementation of `structuredClone`. Before: | (index) | Task name | Latency average (ns) | Latency median (ns) | Throughput average (ops/s) | Throughput median (ops/s) | Samples | | ------- | ------------------------ | -------------------- | ------------------- | -------------------------- | ------------------------- | ------- | | 0 | 'clone a string' | '955.81 ± 1.45%' | '902.00' | '1095925 ± 0.01%' | '1108647' | 1046239 | | 1 | 'clone a basic array' | '7684.79 ± 0.76%' | '7542.00' | '131980 ± 0.02%' | '132591' | 130128 | | 2 | 'clone a basic object' | '6286.35 ± 0.53%' | '6179.00' | '161179 ± 0.02%' | '161838' | 159075 | | 3 | 'clone a complex object' | '22446.96 ± 0.32%' | '22223.00' | '44819 ± 0.03%' | '44998' | 44550 | After: | (index) | Task name | Latency average (ns) | Latency median (ns) | Throughput average (ops/s) | Throughput median (ops/s) | Samples | | ------- | ------------------------ | -------------------- | ------------------- | -------------------------- | ------------------------- | ------- | | 0 | 'clone a string' | '793.81 ± 0.09%' | '781.00' | '1270316 ± 0.01%' | '1280410' | 1259755 | | 1 | 'clone a basic array' | '5079.81 ± 2.13%' | '4798.00' | '206235 ± 0.03%' | '208420' | 196858 | | 2 | 'clone a basic object' | '3823.37 ± 1.60%' | '3636.00' | '271030 ± 0.03%' | '275028' | 261550 | | 3 | 'clone a complex object' | '13095.13 ± 2.04%' | '12499.00' | '79701 ± 0.03%' | '80006' | 76365 | Reviewed By: hoxyq Differential Revision: D74576514 fbshipit-source-id: f452dcfca6398dbfa40c5a41b77b0157777fa59b --- .../structuredClone/structuredClone.js | 138 ++++++++++-------- 1 file changed, 77 insertions(+), 61 deletions(-) diff --git a/packages/react-native/src/private/webapis/structuredClone/structuredClone.js b/packages/react-native/src/private/webapis/structuredClone/structuredClone.js index f213261702c..56b1627f803 100644 --- a/packages/react-native/src/private/webapis/structuredClone/structuredClone.js +++ b/packages/react-native/src/private/webapis/structuredClone/structuredClone.js @@ -21,7 +21,18 @@ const VALID_ERROR_NAMES = new Set([ 'URIError', ]); -function structuredCloneInternal(value: T, memory: Map): T { +const BASIC_CONSTRUCTORS = [Number, String, Boolean, Date]; + +const ObjectPrototype = Object.prototype; + +// Technicall the memory value should be a parameter in +// `structuredCloneInternal` but as an optimization we can reuse the same map +// and avoid allocating a new one in every call to `structuredClone`. +// This is safe because we don't invoke user code in `structuredClone`, so at +// any given point we only have one memory object alive anyway. +const memory: Map = new Map(); + +function structuredCloneInternal(value: T): T { // Handles `null` and `undefined`. if (value == null) { return value; @@ -51,54 +62,68 @@ function structuredCloneInternal(value: T, memory: Map): T { return memory.get(value); } - // Known non-serializable objects - // TODO: Handle this more holistically. - if ( - value instanceof WeakMap || - value instanceof WeakSet || - value instanceof Promise - ) { - throw new DOMException( - `Failed to execute 'structuredClone' on 'Window': ${String(value)} could not be cloned.`, - 'DataCloneError', - ); - } - - // Handles primitive wrappers. - - if (value instanceof Number) { - // eslint-disable-next-line no-new-wrappers - const result = new Number(value); + // Handles arrays. + if (Array.isArray(value)) { + const result = []; memory.set(value, result); + for (const key of Object.keys(value)) { + result[key] = structuredCloneInternal(value[key]); + } + // $FlowExpectedError[incompatible-return] we know result is T return result; } - if (value instanceof Boolean) { - // eslint-disable-next-line no-new-wrappers - const result = new Boolean(value); + // Simple object fast path + // $FlowIssue[prop-missing] Why doesn't Flow know about Object.prototype? + if (Object.getPrototypeOf(value) === ObjectPrototype) { + const result = {}; memory.set(value, result); + for (const key of Object.keys(value)) { + // $FlowExpectedError[prop-missing] + result[key] = structuredCloneInternal(value[key]); + } + // $FlowExpectedError[incompatible-return] we know result is T return result; } - if (value instanceof String) { - // eslint-disable-next-line no-new-wrappers - const result = new String(value); + // Handles complex types (typeof === 'object'). + + for (const Cls of BASIC_CONSTRUCTORS) { + if (value instanceof Cls) { + const result = new Cls(value); + memory.set(value, result); + // $FlowExpectedError[incompatible-return] we know result is T + return result; + } + } + + if (value instanceof Map) { + const result = new Map(); memory.set(value, result); + for (const [innerKey, innerValue] of value) { + result.set( + structuredCloneInternal(innerKey), + structuredCloneInternal(innerValue), + ); + } + // $FlowExpectedError[incompatible-return] we know result is T return result; } - // Handles remaining known objects. - - if (value instanceof Date) { - const result = new Date(value); + if (value instanceof Set) { + const result = new Set(); memory.set(value, result); + for (const innerValue of value) { + result.add(structuredCloneInternal(innerValue)); + } + // $FlowExpectedError[incompatible-return] we know result is T return result; } @@ -121,33 +146,6 @@ function structuredCloneInternal(value: T, memory: Map): T { return result; } - if (value instanceof Map) { - const result = new Map(); - memory.set(value, result); - - for (const [innerKey, innerValue] of value) { - result.set( - structuredCloneInternal(innerKey, memory), - structuredCloneInternal(innerValue, memory), - ); - } - - // $FlowExpectedError[incompatible-return] we know result is T - return result; - } - - if (value instanceof Set) { - const result = new Set(); - memory.set(value, result); - - for (const innerValue of value) { - result.add(structuredCloneInternal(innerValue, memory)); - } - - // $FlowExpectedError[incompatible-return] we know result is T - return result; - } - if (value instanceof RegExp) { const result = new RegExp(value.source, value.flags); memory.set(value, result); @@ -156,17 +154,31 @@ function structuredCloneInternal(value: T, memory: Map): T { return result; } - // $FlowExpectedError[incompatible-type] result will be T - const result: T = Array.isArray(value) ? [] : {}; + // Known non-serializable objects. + // TODO: Handle this more holistically + if ( + value instanceof WeakMap || + value instanceof WeakSet || + value instanceof Promise + ) { + throw new DOMException( + `Failed to execute 'structuredClone' on 'Window': ${String(value)} could not be cloned.`, + 'DataCloneError', + ); + } + + // Arbitrary object slow path + const result = {}; memory.set(value, result); // We need to use Object.keys instead of iterating by indices because we // also need to copy arbitrary fields set in the array. for (const key of Object.keys(value)) { - // $FlowExpectedError[incompatible-use] - result[key] = structuredCloneInternal(value[key], memory); + // $FlowExpectedError[prop-missing] + result[key] = structuredCloneInternal(value[key]); } + // $FlowExpectedError[incompatible-return] we know result is T return result; } @@ -190,5 +202,9 @@ function structuredCloneInternal(value: T, memory: Map): T { * - it does not support cloning platform objects like `DOMRect` and `DOMException`. */ export default function structuredClone(value: T): T { - return structuredCloneInternal(value, new Map()); + try { + return structuredCloneInternal(value); + } finally { + memory.clear(); + } }