Skip to content

Commit

Permalink
perf: bail early when traversing non-state (#10654)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
dummdidumm committed Feb 27, 2024
1 parent 6625c1e commit 3fe4940
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 14 deletions.
5 changes: 5 additions & 0 deletions .changeset/chatty-sloths-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte": patch
---

perf: bail early when traversing non-state
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export const javascript_visitors_legacy = {
// If the binding is a prop, we need to deep read it because it could be fine-grained $state
// from a runes-component, where mutations don't trigger an update on the prop as a whole.
if (name === '$$props' || name === '$$restProps' || binding.kind === 'prop') {
serialized = b.call('$.deep_read', serialized);
serialized = b.call('$.deep_read_state', serialized);
}

sequence.push(serialized);
Expand Down
28 changes: 16 additions & 12 deletions packages/svelte/src/internal/client/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ import {
push,
pop,
current_component_context,
deep_read,
get,
set,
is_signals_recorded,
inspect_fn
inspect_fn,
deep_read_state
} from './runtime.js';
import {
render_effect,
Expand Down Expand Up @@ -1950,7 +1950,7 @@ export function action(dom, action, value_fn) {
// This works in legacy mode because of mutable_source being updated as a whole, but when using $state
// together with actions and mutation, it wouldn't notice the change without a deep read.
if (needs_deep_read) {
deep_read(value);
deep_read_state(value);
}
} else {
untrack(() => (payload = action(dom)));
Expand Down Expand Up @@ -2858,10 +2858,12 @@ export function init() {
if (!callbacks) return;

// beforeUpdate
pre_effect(() => {
observe_all(context);
callbacks.b.forEach(run);
});
if (callbacks.b.length) {
pre_effect(() => {
observe_all(context);
callbacks.b.forEach(run);
});
}

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

// afterUpdate
user_effect(() => {
observe_all(context);
callbacks.a.forEach(run);
});
if (callbacks.a.length) {
user_effect(() => {
observe_all(context);
callbacks.a.forEach(run);
});
}
}

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

deep_read(context.s);
deep_read_state(context.s);
}

/**
Expand Down
23 changes: 23 additions & 0 deletions packages/svelte/src/internal/client/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,29 @@ export function pop(component) {
return component || /** @type {T} */ ({});
}

/**
* Possibly traverse an object and read all its properties so that they're all reactive in case this is `$state`.
* Does only check first level of an object for performance reasons (heuristic should be good for 99% of all cases).
* @param {any} value
* @returns {void}
*/
export function deep_read_state(value) {
if (typeof value !== 'object' || !value || value instanceof EventTarget) {
return;
}

if (STATE_SYMBOL in value) {
deep_read(value);
} else if (!Array.isArray(value)) {
for (let key in value) {
const prop = value[key];
if (typeof prop === 'object' && prop && STATE_SYMBOL in prop) {
deep_read(prop);
}
}
}
}

/**
* Deeply traverse an object and read all its properties
* so that they're all reactive in case this is `$state`
Expand Down
3 changes: 2 additions & 1 deletion packages/svelte/src/internal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ export {
inspect,
unwrap,
freeze,
deep_read
deep_read,
deep_read_state
} from './client/runtime.js';
export * from './client/dev/ownership.js';
export { await_block as await } from './client/dom/blocks/await.js';
Expand Down

0 comments on commit 3fe4940

Please sign in to comment.