Skip to content

Commit dc787be

Browse files
authored
chore: improve signal perf by using Set rather than array for reactions (#12831)
* chore: improve signal perf by using Set rather than array for reactions * tweak * simplify * lint * address feedback
1 parent 873a184 commit dc787be

File tree

5 files changed

+24
-39
lines changed

5 files changed

+24
-39
lines changed

.changeset/calm-clocks-raise.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 perf by using Set rather than array for reactions

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,8 @@ function mark_reactions(signal, status) {
139139
if (reactions === null) return;
140140

141141
var runes = is_runes();
142-
var length = reactions.length;
143142

144-
for (var i = 0; i < length; i++) {
145-
var reaction = reactions[i];
143+
for (var reaction of reactions) {
146144
var flags = reaction.f;
147145

148146
// Skip any effects that are already dirty

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export interface Signal {
99

1010
export interface Value<V = unknown> extends Signal {
1111
/** Signals that read from this signal */
12-
reactions: null | Reaction[];
12+
reactions: null | Set<Reaction>;
1313
/** Equality function */
1414
equals: Equals;
1515
/** The latest value for this signal */

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

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export function check_dirtiness(reaction) {
169169

170170
if ((flags & DISCONNECTED) !== 0) {
171171
for (i = 0; i < dependencies.length; i++) {
172-
(dependencies[i].reactions ??= []).push(reaction);
172+
(dependencies[i].reactions ??= new Set()).add(reaction);
173173
}
174174

175175
reaction.f ^= DISCONNECTED;
@@ -188,11 +188,11 @@ export function check_dirtiness(reaction) {
188188

189189
if (is_unowned) {
190190
// TODO is there a more logical place to do this work?
191-
if (!current_skip_reaction && !dependency?.reactions?.includes(reaction)) {
191+
if (!current_skip_reaction && !dependency?.reactions?.has(reaction)) {
192192
// If we are working with an unowned signal as part of an effect (due to !current_skip_reaction)
193193
// and the version hasn't changed, we still need to check that this reaction
194194
// if linked to the dependency source – otherwise future updates will not be caught.
195-
(dependency.reactions ??= []).push(reaction);
195+
(dependency.reactions ??= new Set()).add(reaction);
196196
}
197197
}
198198
}
@@ -323,17 +323,7 @@ export function update_reaction(reaction) {
323323

324324
if (!current_skip_reaction) {
325325
for (i = skipped_deps; i < deps.length; i++) {
326-
dependency = deps[i];
327-
var reactions = dependency.reactions;
328-
329-
if (reactions === null) {
330-
dependency.reactions = [reaction];
331-
} else if (
332-
reactions[reactions.length - 1] !== reaction &&
333-
!reactions.includes(reaction)
334-
) {
335-
reactions.push(reaction);
336-
}
326+
(deps[i].reactions ??= new Set()).add(reaction);
337327
}
338328
}
339329
} else if (deps !== null && skipped_deps < deps.length) {
@@ -358,24 +348,16 @@ export function update_reaction(reaction) {
358348
* @returns {void}
359349
*/
360350
function remove_reaction(signal, dependency) {
361-
const reactions = dependency.reactions;
362-
let reactions_length = 0;
351+
let reactions = dependency.reactions;
363352
if (reactions !== null) {
364-
reactions_length = reactions.length - 1;
365-
const index = reactions.indexOf(signal);
366-
if (index !== -1) {
367-
if (reactions_length === 0) {
368-
dependency.reactions = null;
369-
} else {
370-
// Swap with last element and then remove.
371-
reactions[index] = reactions[reactions_length];
372-
reactions.pop();
373-
}
353+
reactions.delete(signal);
354+
if (reactions.size === 0) {
355+
reactions = dependency.reactions = null;
374356
}
375357
}
376358
// If the derived has no reactions, then we can disconnect it from the graph,
377359
// allowing it to either reconnect in the future, or be GC'd by the VM.
378-
if (reactions_length === 0 && (dependency.f & DERIVED) !== 0) {
360+
if (reactions === null && (dependency.f & DERIVED) !== 0) {
379361
set_signal_status(dependency, MAYBE_DIRTY);
380362
// If we are working with a derived that is owned by an effect, then mark it as being
381363
// disconnected.

packages/svelte/tests/signals/test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -236,13 +236,13 @@ describe('signals', () => {
236236
return () => {
237237
flushSync(() => set(count, 1));
238238
// Ensure we're not leaking consumers
239-
assert.deepEqual(count.reactions?.length, 1);
239+
assert.deepEqual(count.reactions?.size, 1);
240240
flushSync(() => set(count, 2));
241241
// Ensure we're not leaking consumers
242-
assert.deepEqual(count.reactions?.length, 1);
242+
assert.deepEqual(count.reactions?.size, 1);
243243
flushSync(() => set(count, 3));
244244
// Ensure we're not leaking consumers
245-
assert.deepEqual(count.reactions?.length, 1);
245+
assert.deepEqual(count.reactions?.size, 1);
246246
assert.deepEqual(log, [0, 1, 2, 3]);
247247
};
248248
});
@@ -304,8 +304,8 @@ describe('signals', () => {
304304
flushSync(() => set(count, 4));
305305
flushSync(() => set(count, 0));
306306
// Ensure we're not leaking consumers
307-
assert.deepEqual(count.reactions?.length, 1);
308-
assert.deepEqual(calc.reactions?.length, 1);
307+
assert.deepEqual(count.reactions?.size, 1);
308+
assert.deepEqual(calc.reactions?.size, 1);
309309
assert.deepEqual(log, [0, 2, 'limit', 0]);
310310
destroy();
311311
// Ensure we're not leaking consumers
@@ -484,7 +484,7 @@ describe('signals', () => {
484484
return () => {
485485
flushSync();
486486
assert.equal(a?.deps?.length, 1);
487-
assert.equal(state?.reactions?.length, 1);
487+
assert.equal(state?.reactions?.size, 1);
488488
destroy();
489489
assert.equal(a?.deps?.length, 1);
490490
assert.equal(state?.reactions, null);
@@ -658,12 +658,12 @@ describe('signals', () => {
658658
});
659659

660660
assert.equal($.get(d), 0);
661-
assert.equal(count.reactions?.length, 1);
661+
assert.equal(count.reactions?.size, 1);
662662
assert.equal(d.deps?.length, 1);
663663

664664
set(count, 1);
665665
assert.equal($.get(d), 2);
666-
assert.equal(count.reactions?.length, 1);
666+
assert.equal(count.reactions?.size, 1);
667667
assert.equal(d.deps?.length, 1);
668668

669669
destroy();

0 commit comments

Comments
 (0)