Skip to content

Commit 9c20eb4

Browse files
authored
chore: improve signal performance by reducing duplicate deps (#14945)
* chore: expand benchmark iterations * chore: improve signal performance by reducing duplicate deps * feedback * oops
1 parent deb362f commit 9c20eb4

File tree

8 files changed

+59
-43
lines changed

8 files changed

+59
-43
lines changed

.changeset/tame-students-retire.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+
chore: improve signal performance by reducing duplicate deps

packages/svelte/src/internal/client/dev/tracing.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,12 @@ function log_entry(signal, entry) {
4242

4343
const type = (signal.f & DERIVED) !== 0 ? '$derived' : '$state';
4444
const current_reaction = /** @type {Reaction} */ (active_reaction);
45-
const status =
46-
signal.version > current_reaction.version || current_reaction.version === 0 ? 'dirty' : 'clean';
45+
const dirty = signal.wv > current_reaction.wv || current_reaction.wv === 0;
4746

4847
// eslint-disable-next-line no-console
4948
console.groupCollapsed(
5049
`%c${type}`,
51-
status !== 'clean'
52-
? 'color: CornflowerBlue; font-weight: bold'
53-
: 'color: grey; font-weight: bold',
50+
dirty ? 'color: CornflowerBlue; font-weight: bold' : 'color: grey; font-weight: bold',
5451
typeof value === 'object' && value !== null && STATE_SYMBOL in value
5552
? snapshot(value, true)
5653
: value

packages/svelte/src/internal/client/reactivity/deriveds.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
set_signal_status,
1717
skip_reaction,
1818
update_reaction,
19-
increment_version,
19+
increment_write_version,
2020
set_active_effect,
2121
component_context
2222
} from '../runtime.js';
@@ -58,8 +58,9 @@ export function derived(fn) {
5858
f: flags,
5959
fn,
6060
reactions: null,
61+
rv: 0,
6162
v: /** @type {V} */ (null),
62-
version: 0,
63+
wv: 0,
6364
parent: parent_derived ?? active_effect
6465
};
6566

@@ -182,7 +183,7 @@ export function update_derived(derived) {
182183

183184
if (!derived.equals(value)) {
184185
derived.v = value;
185-
derived.version = increment_version();
186+
derived.wv = increment_write_version();
186187
}
187188
}
188189

packages/svelte/src/internal/client/reactivity/effects.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ function create_effect(type, fn, sync, push = true) {
110110
prev: null,
111111
teardown: null,
112112
transitions: null,
113-
version: 0
113+
wv: 0
114114
};
115115

116116
if (DEV) {

packages/svelte/src/internal/client/reactivity/sources.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
set_untracked_writes,
1313
set_signal_status,
1414
untrack,
15-
increment_version,
15+
increment_write_version,
1616
update_effect,
1717
derived_sources,
1818
set_derived_sources,
@@ -57,7 +57,8 @@ export function source(v, stack) {
5757
v,
5858
reactions: null,
5959
equals,
60-
version: 0
60+
rv: 0,
61+
wv: 0
6162
};
6263

6364
if (DEV && tracing_mode_flag) {
@@ -169,7 +170,7 @@ export function internal_set(source, value) {
169170
if (!source.equals(value)) {
170171
var old_value = source.v;
171172
source.v = value;
172-
source.version = increment_version();
173+
source.wv = increment_write_version();
173174

174175
if (DEV && tracing_mode_flag) {
175176
source.updated = get_stack('UpdatedAt');

packages/svelte/src/internal/client/reactivity/types.d.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ export interface Signal {
44
/** Flags bitmask */
55
f: number;
66
/** Write version */
7-
version: number;
7+
wv: number;
88
}
99

1010
export interface Value<V = unknown> extends Signal {
11-
/** Signals that read from this signal */
12-
reactions: null | Reaction[];
1311
/** Equality function */
1412
equals: Equals;
13+
/** Signals that read from this signal */
14+
reactions: null | Reaction[];
15+
/** Read version */
16+
rv: number;
1517
/** The latest value for this signal */
1618
v: V;
1719
/** Dev only */

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

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,14 @@ export function set_untracked_writes(value) {
127127
untracked_writes = value;
128128
}
129129

130-
/** @type {number} Used by sources and deriveds for handling updates to unowned deriveds it starts from 1 to differentiate between a created effect and a run one for tracing */
131-
let current_version = 1;
130+
/**
131+
* @type {number} Used by sources and deriveds for handling updates.
132+
* Version starts from 1 so that unowned deriveds differentiate between a created effect and a run one for tracing
133+
**/
134+
let write_version = 1;
135+
136+
/** @type {number} Used to version each read of a source of derived to avoid duplicating depedencies inside a reaction */
137+
let read_version = 0;
132138

133139
// If we are working with a get() chain that has no active container,
134140
// to prevent memory leaks, we skip adding the reaction.
@@ -168,8 +174,8 @@ export function set_dev_current_component_function(fn) {
168174
dev_current_component_function = fn;
169175
}
170176

171-
export function increment_version() {
172-
return ++current_version;
177+
export function increment_write_version() {
178+
return ++write_version;
173179
}
174180

175181
/** @returns {boolean} */
@@ -226,7 +232,7 @@ export function check_dirtiness(reaction) {
226232
update_derived(/** @type {Derived} */ (dependency));
227233
}
228234

229-
if (dependency.version > reaction.version) {
235+
if (dependency.wv > reaction.wv) {
230236
return true;
231237
}
232238
}
@@ -398,6 +404,7 @@ export function update_reaction(reaction) {
398404
skip_reaction = !is_flushing_effect && (flags & UNOWNED) !== 0;
399405
derived_sources = null;
400406
component_context = reaction.ctx;
407+
read_version++;
401408

402409
try {
403410
var result = /** @type {Function} */ (0, reaction.fn)();
@@ -528,7 +535,7 @@ export function update_effect(effect) {
528535
execute_effect_teardown(effect);
529536
var teardown = update_reaction(effect);
530537
effect.teardown = typeof teardown === 'function' ? teardown : null;
531-
effect.version = current_version;
538+
effect.wv = write_version;
532539

533540
var deps = effect.deps;
534541

@@ -540,7 +547,7 @@ export function update_effect(effect) {
540547
for (let i = 0; i < deps.length; i++) {
541548
var dep = deps[i];
542549
if (dep.trace_need_increase) {
543-
dep.version = increment_version();
550+
dep.wv = increment_write_version();
544551
dep.trace_need_increase = undefined;
545552
dep.trace_v = undefined;
546553
}
@@ -880,27 +887,29 @@ export function get(signal) {
880887
e.state_unsafe_local_read();
881888
}
882889
var deps = active_reaction.deps;
890+
if (signal.rv < read_version) {
891+
signal.rv = read_version;
892+
// If the signal is accessing the same dependencies in the same
893+
// order as it did last time, increment `skipped_deps`
894+
// rather than updating `new_deps`, which creates GC cost
895+
if (new_deps === null && deps !== null && deps[skipped_deps] === signal) {
896+
skipped_deps++;
897+
} else if (new_deps === null) {
898+
new_deps = [signal];
899+
} else {
900+
new_deps.push(signal);
901+
}
883902

884-
// If the signal is accessing the same dependencies in the same
885-
// order as it did last time, increment `skipped_deps`
886-
// rather than updating `new_deps`, which creates GC cost
887-
if (new_deps === null && deps !== null && deps[skipped_deps] === signal) {
888-
skipped_deps++;
889-
} else if (new_deps === null) {
890-
new_deps = [signal];
891-
} else {
892-
new_deps.push(signal);
893-
}
894-
895-
if (
896-
untracked_writes !== null &&
897-
active_effect !== null &&
898-
(active_effect.f & CLEAN) !== 0 &&
899-
(active_effect.f & BRANCH_EFFECT) === 0 &&
900-
untracked_writes.includes(signal)
901-
) {
902-
set_signal_status(active_effect, DIRTY);
903-
schedule_effect(active_effect);
903+
if (
904+
untracked_writes !== null &&
905+
active_effect !== null &&
906+
(active_effect.f & CLEAN) !== 0 &&
907+
(active_effect.f & BRANCH_EFFECT) === 0 &&
908+
untracked_writes.includes(signal)
909+
) {
910+
set_signal_status(active_effect, DIRTY);
911+
schedule_effect(active_effect);
912+
}
904913
}
905914
} else if (is_derived && /** @type {Derived} */ (signal).deps === null) {
906915
var derived = /** @type {Derived} */ (signal);

packages/svelte/tests/signals/test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ describe('signals', () => {
296296
const destroy = effect_root(() => {
297297
user_effect(() => {
298298
log.push($.get(calc));
299+
$.get(calc);
299300
});
300301
});
301302

@@ -306,7 +307,7 @@ describe('signals', () => {
306307
flushSync(() => set(count, 4));
307308
flushSync(() => set(count, 0));
308309
// Ensure we're not leaking consumers
309-
assert.deepEqual(count.reactions?.length, 2);
310+
assert.deepEqual(count.reactions?.length, 1);
310311
assert.deepEqual(calc.reactions?.length, 1);
311312
assert.deepEqual(log, [0, 2, 'limit', 0]);
312313
destroy();

0 commit comments

Comments
 (0)