perf: hoist rest_props exclude list as a module-scope Set (#18252)

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 `<Button ...rest />` 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 <rich.harris@vercel.com>
pull/18322/head
Mathias Picker 4 weeks ago committed by GitHub
parent 980c7e2321
commit a40c745fd9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: hoist `rest_props` exclude list as a module-scope `Set`

@ -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

@ -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
};
/**

@ -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<string | symbol, unknown>, exclude: Array<string | symbol>, name?: string }>}}
* @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Set<string | symbol>, 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<string, unknown>} props
* @param {string[]} exclude
* @param {Set<string>} exclude
* @param {string} [name]
* @returns {Record<string, unknown>}
*/

@ -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];

Loading…
Cancel
Save