Skip to content

Commit c39805d

Browse files
authored
feat: detach inert effects (#11955)
* feat: detach inert effects * simplify * avoid adding inert effects to the tree * partial fix * fix * DRY out * optimise * comments * explicit comparisons
1 parent 3bfb302 commit c39805d

File tree

6 files changed

+84
-36
lines changed

6 files changed

+84
-36
lines changed

.changeset/ten-teachers-travel.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+
feat: detach inert effects

packages/svelte/src/internal/client/dom/blocks/await.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { is_promise } from '../../../shared/utils.js';
1+
import { is_promise, noop } from '../../../shared/utils.js';
22
import {
33
current_component_context,
44
flush_sync,
@@ -113,5 +113,9 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
113113
then_effect = branch(() => then_fn(anchor, input));
114114
}
115115
}
116+
117+
// Inert effects are proactively detached from the effect tree. Returning a noop
118+
// teardown function is an easy way to ensure that this is not discarded
119+
return noop;
116120
});
117121
}

packages/svelte/src/internal/client/dom/blocks/svelte-element.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { current_component_context, current_effect } from '../../runtime.js';
1414
import { DEV } from 'esm-env';
1515
import { is_array } from '../../utils.js';
1616
import { push_template_node } from '../template.js';
17+
import { noop } from '../../../shared/utils.js';
1718

1819
/**
1920
* @param {import('#client').Effect} effect
@@ -151,6 +152,9 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
151152
push_template_node(element, parent_effect);
152153
}
153154
}
155+
156+
// See below
157+
return noop;
154158
});
155159
}
156160

@@ -159,5 +163,9 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
159163
set_should_intro(true);
160164

161165
set_current_each_item(previous_each_item);
166+
167+
// Inert effects are proactively detached from the effect tree. Returning a noop
168+
// teardown function is an easy way to ensure that this is not discarded
169+
return noop;
162170
});
163171
}

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

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,30 @@ function create_effect(type, fn, sync) {
9696
effect.component_function = dev_current_component_function;
9797
}
9898

99-
if (current_reaction !== null && !is_root) {
99+
if (sync) {
100+
var previously_flushing_effect = is_flushing_effect;
101+
102+
try {
103+
set_is_flushing_effect(true);
104+
execute_effect(effect);
105+
effect.f |= EFFECT_RAN;
106+
} finally {
107+
set_is_flushing_effect(previously_flushing_effect);
108+
}
109+
} else if (fn !== null) {
110+
schedule_effect(effect);
111+
}
112+
113+
// if an effect has no dependencies, no DOM and no teardown function,
114+
// don't bother adding it to the effect tree
115+
var inert =
116+
sync &&
117+
effect.deps === null &&
118+
effect.first === null &&
119+
effect.dom === null &&
120+
effect.teardown === null;
121+
122+
if (!inert && current_reaction !== null && !is_root) {
100123
var flags = current_reaction.f;
101124
if ((flags & DERIVED) !== 0) {
102125
if ((flags & UNOWNED) !== 0) {
@@ -112,20 +135,6 @@ function create_effect(type, fn, sync) {
112135
push_effect(effect, current_reaction);
113136
}
114137

115-
if (sync) {
116-
var previously_flushing_effect = is_flushing_effect;
117-
118-
try {
119-
set_is_flushing_effect(true);
120-
execute_effect(effect);
121-
effect.f |= EFFECT_RAN;
122-
} finally {
123-
set_is_flushing_effect(previously_flushing_effect);
124-
}
125-
} else if (fn !== null) {
126-
schedule_effect(effect);
127-
}
128-
129138
return effect;
130139
}
131140

@@ -348,23 +357,7 @@ export function destroy_effect(effect, remove_dom = true) {
348357

349358
// If the parent doesn't have any children, then skip this work altogether
350359
if (parent !== null && (effect.f & BRANCH_EFFECT) !== 0 && parent.first !== null) {
351-
var previous = effect.prev;
352-
var next = effect.next;
353-
if (previous !== null) {
354-
if (next !== null) {
355-
previous.next = next;
356-
next.prev = previous;
357-
} else {
358-
previous.next = null;
359-
parent.last = previous;
360-
}
361-
} else if (next !== null) {
362-
next.prev = null;
363-
parent.first = next;
364-
} else {
365-
parent.first = null;
366-
parent.last = null;
367-
}
360+
unlink_effect(effect);
368361
}
369362

370363
// `first` and `child` are nulled out in destroy_effect_children
@@ -379,6 +372,25 @@ export function destroy_effect(effect, remove_dom = true) {
379372
null;
380373
}
381374

375+
/**
376+
* Detach an effect from the effect tree, freeing up memory and
377+
* reducing the amount of work that happens on subsequent traversals
378+
* @param {import('#client').Effect} effect
379+
*/
380+
export function unlink_effect(effect) {
381+
var parent = effect.parent;
382+
var prev = effect.prev;
383+
var next = effect.next;
384+
385+
if (prev !== null) prev.next = next;
386+
if (next !== null) next.prev = prev;
387+
388+
if (parent !== null) {
389+
if (parent.first === effect) parent.first = next;
390+
if (parent.last === effect) parent.last = prev;
391+
}
392+
}
393+
382394
/**
383395
* When a block effect is removed, we don't immediately destroy it or yank it
384396
* out of the DOM, because it might have transitions. Instead, we 'pause' it.

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import {
77
object_freeze
88
} from './utils.js';
99
import { snapshot } from './proxy.js';
10-
import { destroy_effect, effect, execute_effect_teardown } from './reactivity/effects.js';
10+
import {
11+
destroy_effect,
12+
effect,
13+
execute_effect_teardown,
14+
unlink_effect
15+
} from './reactivity/effects.js';
1116
import {
1217
EFFECT,
1318
RENDER_EFFECT,
@@ -603,6 +608,21 @@ function flush_queued_effects(effects) {
603608

604609
if ((effect.f & (DESTROYED | INERT)) === 0 && check_dirtiness(effect)) {
605610
execute_effect(effect);
611+
612+
// Effects with no dependencies or teardown do not get added to the effect tree.
613+
// Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we
614+
// don't know if we need to keep them until they are executed. Doing the check
615+
// here (rather than in `execute_effect`) allows us to skip the work for
616+
// immediate effects.
617+
if (effect.deps === null && effect.first === null && effect.dom === null) {
618+
if (effect.teardown === null) {
619+
// remove this effect from the graph
620+
unlink_effect(effect);
621+
} else {
622+
// keep the effect in the graph, but free up some memory
623+
effect.fn = null;
624+
}
625+
}
606626
}
607627
}
608628
}

packages/svelte/tests/signals/test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@ import { describe, assert, it } from 'vitest';
22
import { flushSync } from '../../src/index-client';
33
import * as $ from '../../src/internal/client/runtime';
44
import {
5-
destroy_effect,
65
effect,
76
effect_root,
87
render_effect,
98
user_effect
109
} from '../../src/internal/client/reactivity/effects';
1110
import { source, set } from '../../src/internal/client/reactivity/sources';
12-
import type { Derived, Effect, Value } from '../../src/internal/client/types';
11+
import type { Derived, Value } from '../../src/internal/client/types';
1312
import { proxy } from '../../src/internal/client/proxy';
1413
import { derived } from '../../src/internal/client/reactivity/deriveds';
1514

0 commit comments

Comments
 (0)