Skip to content

Commit 777527b

Browse files
authored
fix: remove memory leak from retaining old DOM elements (#11197)
* fix: remove memory leak from retaining old DOM elements * missing logic * fix dynamic html bug
1 parent 63456f1 commit 777527b

File tree

6 files changed

+186
-23
lines changed

6 files changed

+186
-23
lines changed

.changeset/rich-plums-thank.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: remove memory leak from retaining old DOM elements

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

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,30 @@
11
import { derived } from '../../reactivity/deriveds.js';
22
import { render_effect } from '../../reactivity/effects.js';
3-
import { get } from '../../runtime.js';
3+
import { current_effect, get } from '../../runtime.js';
4+
import { is_array } from '../../utils.js';
45
import { hydrate_nodes, hydrating } from '../hydration.js';
56
import { create_fragment_from_html, remove } from '../reconciler.js';
7+
import { push_template_node } from '../template.js';
8+
9+
/**
10+
* @param {import('#client').Effect} effect
11+
* @param {(Element | Comment | Text)[]} to_remove
12+
* @returns {void}
13+
*/
14+
function remove_from_parent_effect(effect, to_remove) {
15+
const dom = effect.dom;
16+
17+
if (is_array(dom)) {
18+
for (let i = dom.length - 1; i >= 0; i--) {
19+
if (to_remove.includes(dom[i])) {
20+
dom.splice(i, 1);
21+
break;
22+
}
23+
}
24+
} else if (dom !== null && to_remove.includes(dom)) {
25+
effect.dom = null;
26+
}
27+
}
628

729
/**
830
* @param {Element | Text | Comment} anchor
@@ -11,13 +33,19 @@ import { create_fragment_from_html, remove } from '../reconciler.js';
1133
* @returns {void}
1234
*/
1335
export function html(anchor, get_value, svg) {
36+
const parent_effect = anchor.parentNode !== current_effect?.dom ? current_effect : null;
1437
let value = derived(get_value);
1538

1639
render_effect(() => {
17-
var dom = html_to_dom(anchor, get(value), svg);
40+
var dom = html_to_dom(anchor, parent_effect, get(value), svg);
1841

1942
if (dom) {
20-
return () => remove(dom);
43+
return () => {
44+
if (parent_effect !== null) {
45+
remove_from_parent_effect(parent_effect, is_array(dom) ? dom : [dom]);
46+
}
47+
remove(dom);
48+
};
2149
}
2250
});
2351
}
@@ -27,11 +55,12 @@ export function html(anchor, get_value, svg) {
2755
* inserts it before the target anchor and returns the new nodes.
2856
* @template V
2957
* @param {Element | Text | Comment} target
58+
* @param {import('#client').Effect | null} effect
3059
* @param {V} value
3160
* @param {boolean} svg
3261
* @returns {Element | Comment | (Element | Comment | Text)[]}
3362
*/
34-
function html_to_dom(target, value, svg) {
63+
function html_to_dom(target, effect, value, svg) {
3564
if (hydrating) return hydrate_nodes;
3665

3766
var html = value + '';
@@ -49,6 +78,9 @@ function html_to_dom(target, value, svg) {
4978
if (node.childNodes.length === 1) {
5079
var child = /** @type {Text | Element | Comment} */ (node.firstChild);
5180
target.before(child);
81+
if (effect !== null) {
82+
push_template_node(effect, child);
83+
}
5284
return child;
5385
}
5486

@@ -62,5 +94,9 @@ function html_to_dom(target, value, svg) {
6294
target.before(node);
6395
}
6496

97+
if (effect !== null) {
98+
push_template_node(effect, nodes);
99+
}
100+
65101
return nodes;
66102
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { is_array } from '../../utils.js';
1313
import { set_should_intro } from '../../render.js';
1414
import { current_each_item, set_current_each_item } from './each.js';
1515
import { current_effect } from '../../runtime.js';
16+
import { push_template_node } from '../template.js';
1617

1718
/**
1819
* @param {import('#client').Effect} effect
@@ -131,6 +132,8 @@ export function element(anchor, get_tag, is_svg, render_fn) {
131132
if (prev_element) {
132133
swap_block_dom(parent_effect, prev_element, element);
133134
prev_element.remove();
135+
} else if (!hydrating) {
136+
push_template_node(parent_effect, element);
134137
}
135138
});
136139
}

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

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,36 @@ 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';
8+
9+
/**
10+
* @param {import("#client").Effect} effect
11+
* @param {import("#client").TemplateNode | import("#client").TemplateNode[]} dom
12+
*/
13+
export function push_template_node(effect, dom) {
14+
var current_dom = effect.dom;
15+
if (current_dom === null) {
16+
effect.dom = dom;
17+
} else {
18+
if (!is_array(current_dom)) {
19+
current_dom = effect.dom = [current_dom];
20+
}
21+
var anchor;
22+
// If we're working with an anchor, then remove it and put it at the end.
23+
if (current_dom[0].nodeType === 8) {
24+
anchor = current_dom.pop();
25+
}
26+
if (is_array(dom)) {
27+
current_dom.push(...dom);
28+
} else {
29+
current_dom.push(dom);
30+
}
31+
if (anchor !== undefined) {
32+
current_dom.push(anchor);
33+
}
34+
}
35+
return dom;
36+
}
737

838
/**
939
* @param {string} content
@@ -19,16 +49,31 @@ export function template(content, flags) {
1949
var node;
2050

2151
return () => {
52+
var effect = /** @type {import('#client').Effect} */ (current_effect);
2253
if (hydrating) {
23-
return is_fragment ? hydrate_nodes : /** @type {Node} */ (hydrate_nodes[0]);
54+
var hydration_content = push_template_node(
55+
effect,
56+
is_fragment ? hydrate_nodes : hydrate_nodes[0]
57+
);
58+
return /** @type {Node} */ (hydration_content);
2459
}
2560

2661
if (!node) {
2762
node = create_fragment_from_html(content);
2863
if (!is_fragment) node = /** @type {Node} */ (node.firstChild);
2964
}
65+
var clone = use_import_node ? document.importNode(node, true) : clone_node(node, true);
66+
67+
if (is_fragment) {
68+
push_template_node(
69+
effect,
70+
/** @type {import('#client').TemplateNode[]} */ ([...clone.childNodes])
71+
);
72+
} else {
73+
push_template_node(effect, /** @type {import('#client').TemplateNode} */ (clone));
74+
}
3075

31-
return use_import_node ? document.importNode(node, true) : clone_node(node, true);
76+
return clone;
3277
};
3378
}
3479

@@ -70,8 +115,13 @@ export function svg_template(content, flags) {
70115
var node;
71116

72117
return () => {
118+
var effect = /** @type {import('#client').Effect} */ (current_effect);
73119
if (hydrating) {
74-
return is_fragment ? hydrate_nodes : /** @type {Node} */ (hydrate_nodes[0]);
120+
var hydration_content = push_template_node(
121+
effect,
122+
is_fragment ? hydrate_nodes : hydrate_nodes[0]
123+
);
124+
return /** @type {Node} */ (hydration_content);
75125
}
76126

77127
if (!node) {
@@ -87,7 +137,18 @@ export function svg_template(content, flags) {
87137
}
88138
}
89139

90-
return clone_node(node, true);
140+
var clone = clone_node(node, true);
141+
142+
if (is_fragment) {
143+
push_template_node(
144+
effect,
145+
/** @type {import('#client').TemplateNode[]} */ ([...clone.childNodes])
146+
);
147+
} else {
148+
push_template_node(effect, /** @type {import('#client').TemplateNode} */ (clone));
149+
}
150+
151+
return clone;
91152
};
92153
}
93154

@@ -152,7 +213,8 @@ function run_scripts(node) {
152213
*/
153214
/*#__NO_SIDE_EFFECTS__*/
154215
export function text(anchor) {
155-
if (!hydrating) return empty();
216+
var effect = /** @type {import('#client').Effect} */ (current_effect);
217+
if (!hydrating) return push_template_node(effect, empty());
156218

157219
var node = hydrate_nodes[0];
158220

@@ -162,7 +224,7 @@ export function text(anchor) {
162224
anchor.before((node = empty()));
163225
}
164226

165-
return node;
227+
return push_template_node(effect, node);
166228
}
167229

168230
export const comment = template('<!>', TEMPLATE_FRAGMENT);
@@ -174,19 +236,7 @@ export const comment = template('<!>', TEMPLATE_FRAGMENT);
174236
* @param {import('#client').Dom} dom
175237
*/
176238
export function append(anchor, dom) {
177-
var current = dom;
178-
179239
if (!hydrating) {
180-
var node = /** @type {Node} */ (dom);
181-
182-
if (node.nodeType === 11) {
183-
// if hydrating, `dom` is already an array of nodes, but if not then
184-
// we need to create an array to store it on the current effect
185-
current = /** @type {import('#client').Dom} */ ([...node.childNodes]);
186-
}
187-
188-
anchor.before(node);
240+
anchor.before(/** @type {Node} */ (dom));
189241
}
190-
191-
/** @type {import('#client').Effect} */ (current_effect).dom = current;
192242
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { flushSync } from '../../../../src/index-client';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
html: `<button>add item</button><button>make span</button><button>reverse</button>`,
6+
7+
async test({ assert, target }) {
8+
const [btn1, btn2, btn3] = target.querySelectorAll('button');
9+
10+
flushSync(() => {
11+
btn1?.click();
12+
btn1?.click();
13+
btn1?.click();
14+
});
15+
16+
assert.htmlEqual(
17+
target.innerHTML,
18+
`<button>add item</button><button>make span</button><button>reverse</button><div>Item 1</div><div>Item 2</div><div>Item 3</div>`
19+
);
20+
21+
flushSync(() => {
22+
btn2?.click();
23+
});
24+
25+
assert.htmlEqual(
26+
target.innerHTML,
27+
`<button>add item</button><button>make span</button><button>reverse</button><span>Item 1</span><span>Item 2</span><span>Item 3</span>`
28+
);
29+
30+
flushSync(() => {
31+
btn3?.click();
32+
});
33+
34+
assert.htmlEqual(
35+
target.innerHTML,
36+
`<button>add item</button><button>make span</button><button>reverse</button><span>Item 3</span><span>Item 2</span><span>Item 1</span>`
37+
);
38+
}
39+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<script>
2+
let items = $state([]);
3+
4+
function add_item() {
5+
items.push({
6+
id: items.length,
7+
text: 'Item ' + (items.length + 1),
8+
html: '<div>Item ' + (items.length + 1) + '</div>',
9+
dom: null,
10+
})
11+
}
12+
13+
function make_span() {
14+
items.forEach(item => {
15+
item.html = item.html.replace(/div/g, 'span')
16+
})
17+
}
18+
19+
function reverse() {
20+
items.reverse();
21+
}
22+
</script>
23+
24+
<button on:click={add_item}>add item</button>
25+
<button on:click={make_span}>make span</button>
26+
<button on:click={reverse}>reverse</button>
27+
28+
{#each items as item (item.id)}
29+
{@html item.html}
30+
{/each}

0 commit comments

Comments
 (0)