From a40c745fd95e855a7c667b24ee6bb149783d1813 Mon Sep 17 00:00:00 2001
From: Mathias Picker <48158184+MathiasWP@users.noreply.github.com>
Date: Fri, 29 May 2026 21:55:07 +0200
Subject: [PATCH] perf: hoist rest_props exclude list as a module-scope Set
(#18252)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The compiler emitted an inline string array as the second argument to
`$.rest_props(...)`, and the runtime did a linear
`Array.prototype.includes` on it on every property access via the
rest-props Proxy.
The exclude list only depends on the component definition, not on the
instance, so it can be hoisted to module scope and shared by every
instance. Switching it to a `Set` at the same time makes each lookup
O(1).
For a component like `` rendered N times, this turns
one per-instance allocation (plus a linear search on every rest-prop
access) into one module-scope allocation plus O(1) lookups.
The legacy `$$restProps` path is unchanged — it mutates the exclude list
in its `deleteProperty` trap, so it can't share a hoisted Set across
instances.
---------
Co-authored-by: Rich Harris
---
.changeset/hoist-rest-excludes.md | 5 +++++
.../client/visitors/VariableDeclaration.js | 14 ++++++++++++--
packages/svelte/src/compiler/utils/builders.js | 3 ++-
.../svelte/src/internal/client/reactivity/props.js | 12 ++++++------
.../_expected/client/index.svelte.js | 4 +++-
5 files changed, 28 insertions(+), 10 deletions(-)
create mode 100644 .changeset/hoist-rest-excludes.md
diff --git a/.changeset/hoist-rest-excludes.md b/.changeset/hoist-rest-excludes.md
new file mode 100644
index 0000000000..52efb06092
--- /dev/null
+++ b/.changeset/hoist-rest-excludes.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+perf: hoist `rest_props` exclude list as a module-scope `Set`
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js
index 72685a8e83..b9f4690179 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js
@@ -49,8 +49,13 @@ export function VariableDeclaration(node, context) {
}
if (declarator.id.type === 'Identifier') {
+ const exclude_id = context.state.scope.root.unique('rest_excludes');
+ context.state.hoisted.push(
+ b.var(exclude_id, b.new('Set', b.array(seen.map((name) => b.literal(name)))))
+ );
+
/** @type {Expression[]} */
- const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
+ const args = [b.id('$$props'), exclude_id];
if (dev) {
// include rest name, so we can provide informative error messages
@@ -95,8 +100,13 @@ export function VariableDeclaration(node, context) {
}
} else {
// RestElement
+ const exclude_id = context.state.scope.root.unique('rest_excludes');
+ context.state.hoisted.push(
+ b.var(exclude_id, b.new('Set', b.array(seen.map((name) => b.literal(name)))))
+ );
+
/** @type {Expression[]} */
- const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
+ const args = [b.id('$$props'), exclude_id];
if (dev) {
// include rest name, so we can provide informative error messages
diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js
index 7508caf3e7..1f48f7fd8b 100644
--- a/packages/svelte/src/compiler/utils/builders.js
+++ b/packages/svelte/src/compiler/utils/builders.js
@@ -686,7 +686,8 @@ export {
if_builder as if,
this_instance as this,
null_instance as null,
- debugger_builder as debugger
+ debugger_builder as debugger,
+ new_builder as new
};
/**
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index 5626639a84..c2f3698809 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -49,11 +49,11 @@ export function update_pre_prop(fn, d = 1) {
/**
* The proxy handler for rest props (i.e. `const { x, ...rest } = $props()`).
* Is passed the full `$$props` object and excludes the named props.
- * @type {ProxyHandler<{ props: Record, exclude: Array, name?: string }>}}
+ * @type {ProxyHandler<{ props: Record, exclude: Set, name?: string }>}}
*/
const rest_props_handler = {
get(target, key) {
- if (target.exclude.includes(key)) return;
+ if (target.exclude.has(key)) return;
return target.props[key];
},
set(target, key) {
@@ -65,7 +65,7 @@ const rest_props_handler = {
return false;
},
getOwnPropertyDescriptor(target, key) {
- if (target.exclude.includes(key)) return;
+ if (target.exclude.has(key)) return;
if (key in target.props) {
return {
enumerable: true,
@@ -75,17 +75,17 @@ const rest_props_handler = {
}
},
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
* @param {string} [name]
* @returns {Record}
*/
diff --git a/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js
index 5a46b9bbef..7df616f694 100644
--- a/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js
@@ -1,10 +1,12 @@
import 'svelte/internal/disclose-version';
import * as $ from 'svelte/internal/client';
+var rest_excludes = new Set(['$$slots', '$$events', '$$legacy']);
+
export default function Props_identifier($$anchor, $$props) {
$.push($$props, true);
- let props = $.rest_props($$props, ['$$slots', '$$events', '$$legacy']);
+ let props = $.rest_props($$props, rest_excludes);
$$props.a;
props[a];