From da0f4328d117efc8ff3753be0808aaa0d1e8ffb6 Mon Sep 17 00:00:00 2001 From: ljodea Date: Wed, 29 Jul 2026 17:29:48 -0500 Subject: [PATCH] perf: use Set for rest-prop exclude lists (server, legacy) Client runes rest_props already takes a Set (#18252). Align server rest_props and legacy_rest_props so exclude membership is O(1). Compiler emits new Set([...]) for both paths. Legacy exclude Sets stay per-instance because deleteProperty mutates them via .add. exclude_from_object left alone: its exclude list is rebuilt per update with tiny key counts, where Set construction can lose to includes. Closes #18601 (server + legacy paths; exclude_from_object deferred). --- .changeset/rest-props-exclude-set.md | 5 ++ .../3-transform/client/transform-client.js | 6 ++- .../3-transform/server/transform-server.js | 2 +- .../src/internal/client/reactivity/props.js | 16 +++---- packages/svelte/src/internal/server/index.js | 4 +- packages/svelte/tests/rest-props/test.ts | 47 +++++++++++++++++++ 6 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 .changeset/rest-props-exclude-set.md create mode 100644 packages/svelte/tests/rest-props/test.ts diff --git a/.changeset/rest-props-exclude-set.md b/.changeset/rest-props-exclude-set.md new file mode 100644 index 0000000000..9f1c32eff5 --- /dev/null +++ b/.changeset/rest-props-exclude-set.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +perf: use Set for rest-prop exclude lists (server, legacy) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 552fe89960..b917478dfe 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -457,7 +457,8 @@ export function client_component(analysis, options) { b.call( '$.legacy_rest_props', b.id('$$sanitized_props'), - b.array(named_props.map((name) => b.literal(name))) + // Per-instance Set: deleteProperty mutates exclude via .add + b.new('Set', b.array(named_props.map((name) => b.literal(name)))) ) ) ); @@ -477,7 +478,8 @@ export function client_component(analysis, options) { component_block.body.unshift( b.const( '$$sanitized_props', - b.call('$.legacy_rest_props', b.id('$$props'), b.array(to_remove)) + // Per-instance Set: deleteProperty mutates exclude via .add + b.call('$.legacy_rest_props', b.id('$$props'), b.new('Set', b.array(to_remove))) ) ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 24ca58bb41..520474de4e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -284,7 +284,7 @@ export function server_component(analysis, options) { b.call( '$.rest_props', b.id('$$sanitized_props'), - b.array(named_props.map((name) => b.literal(name))) + b.new('Set', b.array(named_props.map((name) => b.literal(name)))) ) ) ); diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 274d780b1e..7687d59fea 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -96,11 +96,11 @@ export function rest_props(props, exclude, name) { /** * The proxy handler for legacy $$restProps and $$props - * @type {ProxyHandler<{ props: Record, exclude: Array, special: Record unknown>, version: Source, parent_effect: Effect }>}} + * @type {ProxyHandler<{ props: Record, exclude: Set, special: Record unknown>, version: Source, parent_effect: Effect }>}} */ const legacy_rest_props_handler = { get(target, key) { - if (target.exclude.includes(key)) return; + if (target.exclude.has(key)) return; get(target.version); return key in target.special ? target.special[key]() : target.props[key]; }, @@ -132,7 +132,7 @@ const legacy_rest_props_handler = { return true; }, getOwnPropertyDescriptor(target, key) { - if (target.exclude.includes(key)) return; + if (target.exclude.has(key)) return; if (key in target.props) { return { enumerable: true, @@ -143,23 +143,23 @@ const legacy_rest_props_handler = { }, deleteProperty(target, key) { // Svelte 4 allowed for deletions on $$restProps - if (target.exclude.includes(key)) return true; - target.exclude.push(key); + if (target.exclude.has(key)) return true; + target.exclude.add(key); update(target.version); return true; }, has(target, key) { - if (target.exclude.includes(key)) return false; + if (target.exclude.has(key)) return false; return key in target.props; }, ownKeys(target) { - return Reflect.ownKeys(target.props).filter((key) => !target.exclude.includes(key)); + return Reflect.ownKeys(target.props).filter((key) => !target.exclude.has(key)); } }; /** * @param {Record} props - * @param {string[]} exclude + * @param {Set} exclude * @returns {Record} */ export function legacy_rest_props(props, exclude) { diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 12f76f188e..0d1e6ab368 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -343,7 +343,7 @@ export function slot(renderer, $$props, name, slot_props, fallback_fn) { /** * @param {Record} props - * @param {string[]} rest + * @param {Set} rest * @returns {Record} */ export function rest_props(props, rest) { @@ -351,7 +351,7 @@ export function rest_props(props, rest) { const rest_props = {}; let key; for (key of Object.keys(props)) { - if (!rest.includes(key)) { + if (!rest.has(key)) { rest_props[key] = props[key]; } } diff --git a/packages/svelte/tests/rest-props/test.ts b/packages/svelte/tests/rest-props/test.ts new file mode 100644 index 0000000000..4a2a2ae3e3 --- /dev/null +++ b/packages/svelte/tests/rest-props/test.ts @@ -0,0 +1,47 @@ +import { describe, assert, it } from 'vitest'; +import { rest_props as server_rest_props } from '../../src/internal/server/index.js'; +import { legacy_rest_props } from '../../src/internal/client/reactivity/props.js'; + +/** + * #18601 — rest-prop exclude lists accept `Set` and use O(1) membership. + * These helpers are compiler-owned; the compiler emits `new Set([...])`. + */ +describe('server rest_props exclude Set', () => { + it('omits excluded keys', () => { + const result = server_rest_props({ a: 1, b: 2, c: 3 }, new Set(['a', 'b'])); + assert.deepEqual(result, { c: 3 }); + }); + + it('returns all keys when exclude is empty', () => { + const result = server_rest_props({ a: 1, b: 2 }, new Set()); + assert.deepEqual(result, { a: 1, b: 2 }); + }); + + it('returns empty object when all keys are excluded', () => { + const result = server_rest_props({ a: 1 }, new Set(['a'])); + assert.deepEqual(result, {}); + }); +}); + +describe('legacy_rest_props exclude Set', () => { + it('omits excluded keys from get/has/ownKeys', () => { + const rest = legacy_rest_props({ a: 1, b: 2, c: 3 }, new Set(['a'])); + + assert.equal(rest.a, undefined); + assert.equal(rest.b, 2); + assert.equal(rest.c, 3); + assert.equal('a' in rest, false); + assert.equal('b' in rest, true); + assert.deepEqual(Object.keys(rest).sort(), ['b', 'c']); + }); + + it('deleteProperty adds the key to the exclude Set', () => { + const rest = legacy_rest_props({ a: 1, b: 2 }, new Set()); + + assert.equal(rest.a, 1); + delete rest.a; + assert.equal(rest.a, undefined); + assert.equal('a' in rest, false); + assert.deepEqual(Object.keys(rest), ['b']); + }); +});