Skip to content

Commit 22e2aec

Browse files
authored
fix: address regressed memory leak (#11832)
* fix: address regressed memory leak * fix: address regressed memory leak
1 parent bcb3193 commit 22e2aec

File tree

4 files changed

+90
-20
lines changed

4 files changed

+90
-20
lines changed

.changeset/sour-geese-listen.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+
fix: address regressed memory leak

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { current_effect, get } from '../../runtime.js';
44
import { is_array } from '../../utils.js';
55
import { hydrate_nodes, hydrating } from '../hydration.js';
66
import { create_fragment_from_html, remove } from '../reconciler.js';
7+
import { push_template_node } from '../template.js';
78

89
/**
910
* @param {import('#client').Effect} effect
@@ -37,7 +38,7 @@ export function html(anchor, get_value, svg, mathml) {
3738
let value = derived(get_value);
3839

3940
render_effect(() => {
40-
var dom = html_to_dom(anchor, get(value), svg, mathml);
41+
var dom = html_to_dom(anchor, parent_effect, get(value), svg, mathml);
4142

4243
if (dom) {
4344
return () => {
@@ -55,12 +56,13 @@ export function html(anchor, get_value, svg, mathml) {
5556
* inserts it before the target anchor and returns the new nodes.
5657
* @template V
5758
* @param {Element | Text | Comment} target
59+
* @param {import('#client').Effect | null} effect
5860
* @param {V} value
5961
* @param {boolean} svg
6062
* @param {boolean} mathml
6163
* @returns {Element | Comment | (Element | Comment | Text)[]}
6264
*/
63-
function html_to_dom(target, value, svg, mathml) {
65+
function html_to_dom(target, effect, value, svg, mathml) {
6466
if (hydrating) return hydrate_nodes;
6567

6668
var html = value + '';
@@ -79,6 +81,9 @@ function html_to_dom(target, value, svg, mathml) {
7981
if (node.childNodes.length === 1) {
8082
var child = /** @type {Text | Element | Comment} */ (node.firstChild);
8183
target.before(child);
84+
if (effect !== null) {
85+
push_template_node(child, effect);
86+
}
8287
return child;
8388
}
8489

@@ -92,5 +97,9 @@ function html_to_dom(target, value, svg, mathml) {
9297
target.before(node);
9398
}
9499

100+
if (effect !== null) {
101+
push_template_node(nodes, effect);
102+
}
103+
95104
return nodes;
96105
}

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

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,31 @@ import {
1010
} from '../../reactivity/effects.js';
1111
import { set_should_intro } from '../../render.js';
1212
import { current_each_item, set_current_each_item } from './each.js';
13-
import { current_component_context } from '../../runtime.js';
13+
import { current_component_context, current_effect } from '../../runtime.js';
1414
import { DEV } from 'esm-env';
15+
import { is_array } from '../../utils.js';
16+
import { push_template_node } from '../template.js';
17+
18+
/**
19+
* @param {import('#client').Effect} effect
20+
* @param {Element} from
21+
* @param {Element} to
22+
* @returns {void}
23+
*/
24+
function swap_block_dom(effect, from, to) {
25+
const dom = effect.dom;
26+
27+
if (is_array(dom)) {
28+
for (let i = 0; i < dom.length; i++) {
29+
if (dom[i] === from) {
30+
dom[i] = to;
31+
break;
32+
}
33+
}
34+
} else if (dom === from) {
35+
effect.dom = to;
36+
}
37+
}
1538

1639
/**
1740
* @param {Comment} anchor
@@ -23,6 +46,7 @@ import { DEV } from 'esm-env';
2346
* @returns {void}
2447
*/
2548
export function element(anchor, get_tag, is_svg, render_fn, get_namespace, location) {
49+
const parent_effect = /** @type {import('#client').Effect} */ (current_effect);
2650
const filename = DEV && location && current_component_context?.function.filename;
2751

2852
/** @type {string | null} */
@@ -78,6 +102,7 @@ export function element(anchor, get_tag, is_svg, render_fn, get_namespace, locat
78102

79103
if (next_tag && next_tag !== current_tag) {
80104
effect = branch(() => {
105+
const prev_element = element;
81106
element = hydrating
82107
? /** @type {Element} */ (hydrate_start)
83108
: ns
@@ -111,9 +136,12 @@ export function element(anchor, get_tag, is_svg, render_fn, get_namespace, locat
111136

112137
anchor.before(element);
113138

114-
return () => {
115-
element?.remove();
116-
};
139+
if (prev_element) {
140+
swap_block_dom(parent_effect, prev_element, element);
141+
prev_element.remove();
142+
} else if (!hydrating) {
143+
push_template_node(element, parent_effect);
144+
}
117145
});
118146
}
119147

packages/svelte/src/internal/client/dom/template.js

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,32 @@ import { create_fragment_from_html } from './reconciler.js';
44
import { current_effect } from '../runtime.js';
55
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
66
import { effect } from '../reactivity/effects.js';
7+
import { is_array } from '../utils.js';
78

89
/**
910
* @template {import("#client").TemplateNode | import("#client").TemplateNode[]} T
1011
* @param {T} dom
12+
* @param {import("#client").Effect} effect
1113
*/
12-
function push_template_node(dom) {
13-
var effect = /** @type {import('#client').Effect} */ (current_effect);
14-
15-
if (effect.dom === null) {
14+
export function push_template_node(
15+
dom,
16+
effect = /** @type {import('#client').Effect} */ (current_effect)
17+
) {
18+
var current_dom = effect.dom;
19+
if (current_dom === null) {
1620
effect.dom = dom;
21+
} else {
22+
if (!is_array(current_dom)) {
23+
current_dom = effect.dom = [current_dom];
24+
}
25+
26+
if (is_array(dom)) {
27+
current_dom.push(...dom);
28+
} else {
29+
current_dom.push(dom);
30+
}
1731
}
32+
return dom;
1833
}
1934

2035
/**
@@ -41,7 +56,15 @@ export function template(content, flags) {
4156
if (!is_fragment) node = /** @type {Node} */ (node.firstChild);
4257
}
4358

44-
return use_import_node ? document.importNode(node, true) : node.cloneNode(true);
59+
var clone = use_import_node ? document.importNode(node, true) : node.cloneNode(true);
60+
61+
push_template_node(
62+
is_fragment
63+
? /** @type {import('#client').TemplateNode[]} */ ([...clone.childNodes])
64+
: /** @type {import('#client').TemplateNode} */ (clone)
65+
);
66+
67+
return clone;
4568
};
4669
}
4770

@@ -102,7 +125,15 @@ export function ns_template(content, flags, ns = 'svg') {
102125
}
103126
}
104127

105-
return node.cloneNode(true);
128+
var clone = node.cloneNode(true);
129+
130+
push_template_node(
131+
is_fragment
132+
? /** @type {import('#client').TemplateNode[]} */ ([...clone.childNodes])
133+
: /** @type {import('#client').TemplateNode} */ (clone)
134+
);
135+
136+
return clone;
106137
};
107138
}
108139

@@ -177,7 +208,7 @@ function run_scripts(node) {
177208
*/
178209
/*#__NO_SIDE_EFFECTS__*/
179210
export function text(anchor) {
180-
if (!hydrating) return empty();
211+
if (!hydrating) return push_template_node(empty());
181212

182213
var node = hydrate_start;
183214

@@ -201,6 +232,7 @@ export function comment() {
201232
var frag = document.createDocumentFragment();
202233
var anchor = empty();
203234
frag.append(anchor);
235+
push_template_node([anchor]);
204236

205237
return frag;
206238
}
@@ -213,13 +245,9 @@ export function comment() {
213245
*/
214246
export function append(anchor, dom) {
215247
if (hydrating) return;
216-
217-
var effect = /** @type {import('#client').Effect} */ (current_effect);
218-
219-
effect.dom =
220-
dom.nodeType === 11
221-
? /** @type {import('#client').TemplateNode[]} */ ([...dom.childNodes])
222-
: /** @type {Element | Comment} */ (dom);
248+
// We intentionally do not assign the `dom` property of the effect here because it's far too
249+
// late. If we try, we will capture additional DOM elements that we cannot control the lifecycle
250+
// for and will inevitably cause memory leaks. See https://github.com/sveltejs/svelte/pull/11832
223251

224252
anchor.before(/** @type {Node} */ (dom));
225253
}

0 commit comments

Comments
 (0)