Skip to content

Commit 3fe4940

Browse files
authored
perf: bail early when traversing non-state (#10654)
This has a lot of overhead for large lists, and we can at least diminish in the "no state proxy" case by applying a sensible heuristic: - If the value passed is a state proxy, read it - If not, and if the value is an array, then bail because an array of state proxies is highly unlikely - Traverse the first level of properties of the object and look if these are state, if not bail. State proxies nested further down are highly unlikely, too part of #10637
1 parent 6625c1e commit 3fe4940

File tree

5 files changed

+47
-14
lines changed

5 files changed

+47
-14
lines changed

.changeset/chatty-sloths-allow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
perf: bail early when traversing non-state

packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-legacy.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ export const javascript_visitors_legacy = {
170170
// If the binding is a prop, we need to deep read it because it could be fine-grained $state
171171
// from a runes-component, where mutations don't trigger an update on the prop as a whole.
172172
if (name === '$$props' || name === '$$restProps' || binding.kind === 'prop') {
173-
serialized = b.call('$.deep_read', serialized);
173+
serialized = b.call('$.deep_read_state', serialized);
174174
}
175175

176176
sequence.push(serialized);

packages/svelte/src/internal/client/render.js

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ import {
4444
push,
4545
pop,
4646
current_component_context,
47-
deep_read,
4847
get,
4948
set,
5049
is_signals_recorded,
51-
inspect_fn
50+
inspect_fn,
51+
deep_read_state
5252
} from './runtime.js';
5353
import {
5454
render_effect,
@@ -1950,7 +1950,7 @@ export function action(dom, action, value_fn) {
19501950
// This works in legacy mode because of mutable_source being updated as a whole, but when using $state
19511951
// together with actions and mutation, it wouldn't notice the change without a deep read.
19521952
if (needs_deep_read) {
1953-
deep_read(value);
1953+
deep_read_state(value);
19541954
}
19551955
} else {
19561956
untrack(() => (payload = action(dom)));
@@ -2858,10 +2858,12 @@ export function init() {
28582858
if (!callbacks) return;
28592859

28602860
// beforeUpdate
2861-
pre_effect(() => {
2862-
observe_all(context);
2863-
callbacks.b.forEach(run);
2864-
});
2861+
if (callbacks.b.length) {
2862+
pre_effect(() => {
2863+
observe_all(context);
2864+
callbacks.b.forEach(run);
2865+
});
2866+
}
28652867

28662868
// onMount (must run before afterUpdate)
28672869
user_effect(() => {
@@ -2876,10 +2878,12 @@ export function init() {
28762878
});
28772879

28782880
// afterUpdate
2879-
user_effect(() => {
2880-
observe_all(context);
2881-
callbacks.a.forEach(run);
2882-
});
2881+
if (callbacks.a.length) {
2882+
user_effect(() => {
2883+
observe_all(context);
2884+
callbacks.a.forEach(run);
2885+
});
2886+
}
28832887
}
28842888

28852889
/**
@@ -2892,7 +2896,7 @@ function observe_all(context) {
28922896
for (const signal of context.d) get(signal);
28932897
}
28942898

2895-
deep_read(context.s);
2899+
deep_read_state(context.s);
28962900
}
28972901

28982902
/**

packages/svelte/src/internal/client/runtime.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,6 +1244,29 @@ export function pop(component) {
12441244
return component || /** @type {T} */ ({});
12451245
}
12461246

1247+
/**
1248+
* Possibly traverse an object and read all its properties so that they're all reactive in case this is `$state`.
1249+
* Does only check first level of an object for performance reasons (heuristic should be good for 99% of all cases).
1250+
* @param {any} value
1251+
* @returns {void}
1252+
*/
1253+
export function deep_read_state(value) {
1254+
if (typeof value !== 'object' || !value || value instanceof EventTarget) {
1255+
return;
1256+
}
1257+
1258+
if (STATE_SYMBOL in value) {
1259+
deep_read(value);
1260+
} else if (!Array.isArray(value)) {
1261+
for (let key in value) {
1262+
const prop = value[key];
1263+
if (typeof prop === 'object' && prop && STATE_SYMBOL in prop) {
1264+
deep_read(prop);
1265+
}
1266+
}
1267+
}
1268+
}
1269+
12471270
/**
12481271
* Deeply traverse an object and read all its properties
12491272
* so that they're all reactive in case this is `$state`

packages/svelte/src/internal/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export {
1818
inspect,
1919
unwrap,
2020
freeze,
21-
deep_read
21+
deep_read,
22+
deep_read_state
2223
} from './client/runtime.js';
2324
export * from './client/dev/ownership.js';
2425
export { await_block as await } from './client/dom/blocks/await.js';

0 commit comments

Comments
 (0)