Skip to content

Commit fbb7da7

Browse files
authoredJul 2, 2024
feat: simpler effect DOM boundaries (#12258)
* simpler effect dom boundaries * remove unused argument * tidy up * simplify * skip redundant comment templates for components (and others TODO) * same optimisation for render tags * DRY out * appease typescript * changeset * tighten up, leave note to self * reinstate $.comment optimisation * add explanation * comments
1 parent dcc7ed4 commit fbb7da7

File tree

26 files changed

+203
-235
lines changed

26 files changed

+203
-235
lines changed
 

‎.changeset/beige-gifts-appear.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: simpler effect DOM boundaries

‎packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js‎

Lines changed: 21 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import {
3636
EACH_KEYED,
3737
is_capture_event,
3838
TEMPLATE_FRAGMENT,
39-
TEMPLATE_UNSET_START,
4039
TEMPLATE_USE_IMPORT_NODE,
4140
TRANSITION_GLOBAL,
4241
TRANSITION_IN,
@@ -1561,7 +1560,7 @@ export const template_visitors = {
15611560

15621561
const namespace = infer_namespace(context.state.metadata.namespace, parent, node.nodes);
15631562

1564-
const { hoisted, trimmed } = clean_nodes(
1563+
const { hoisted, trimmed, is_standalone } = clean_nodes(
15651564
parent,
15661565
node.nodes,
15671566
context.path,
@@ -1676,56 +1675,38 @@ export const template_visitors = {
16761675
);
16771676
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
16781677
} else {
1679-
/** @type {(is_text: boolean) => import('estree').Expression} */
1680-
const expression = (is_text) =>
1681-
is_text ? b.call('$.first_child', id, b.true) : b.call('$.first_child', id);
1682-
1683-
process_children(trimmed, expression, false, { ...context, state });
1684-
1685-
var first = trimmed[0];
1686-
1687-
/**
1688-
* If the first item in an effect is a static slot or render tag, it will clone
1689-
* a template but without creating a child effect. In these cases, we need to keep
1690-
* the current `effect.nodes.start` undefined, so that it can be populated by
1691-
* the item in question
1692-
* TODO come up with a better name than `unset`
1693-
*/
1694-
var unset = false;
1695-
1696-
if (first.type === 'SlotElement') unset = true;
1697-
if (first.type === 'RenderTag' && !first.metadata.dynamic) unset = true;
1698-
if (first.type === 'Component' && !first.metadata.dynamic && !context.state.options.hmr) {
1699-
unset = true;
1700-
}
1678+
if (is_standalone) {
1679+
// no need to create a template, we can just use the existing block's anchor
1680+
process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state });
1681+
} else {
1682+
/** @type {(is_text: boolean) => import('estree').Expression} */
1683+
const expression = (is_text) =>
1684+
is_text ? b.call('$.first_child', id, b.true) : b.call('$.first_child', id);
17011685

1702-
const use_comment_template = state.template.length === 1 && state.template[0] === '<!>';
1686+
process_children(trimmed, expression, false, { ...context, state });
17031687

1704-
if (use_comment_template) {
1705-
// special case — we can use `$.comment` instead of creating a unique template
1706-
body.push(b.var(id, b.call('$.comment', unset && b.literal(unset))));
1707-
} else {
17081688
let flags = TEMPLATE_FRAGMENT;
17091689

1710-
if (unset) {
1711-
flags |= TEMPLATE_UNSET_START;
1712-
}
1713-
17141690
if (state.metadata.context.template_needs_import_node) {
17151691
flags |= TEMPLATE_USE_IMPORT_NODE;
17161692
}
17171693

1718-
add_template(template_name, [
1719-
b.template([b.quasi(state.template.join(''), true)], []),
1720-
b.literal(flags)
1721-
]);
1694+
if (state.template.length === 1 && state.template[0] === '<!>') {
1695+
// special case — we can use `$.comment` instead of creating a unique template
1696+
body.push(b.var(id, b.call('$.comment')));
1697+
} else {
1698+
add_template(template_name, [
1699+
b.template([b.quasi(state.template.join(''), true)], []),
1700+
b.literal(flags)
1701+
]);
1702+
1703+
body.push(b.var(id, b.call(template_name)));
1704+
}
17221705

1723-
body.push(b.var(id, b.call(template_name)));
1706+
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
17241707
}
17251708

17261709
body.push(...state.before_init, ...state.init);
1727-
1728-
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
17291710
}
17301711
} else {
17311712
body.push(...state.before_init, ...state.init);

‎packages/svelte/src/compiler/phases/3-transform/server/transform-server.js‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,8 @@ function serialize_inline_component(node, expression, context) {
10021002
)
10031003
);
10041004

1005+
context.state.template.push(statement);
1006+
} else if (context.state.skip_hydration_boundaries) {
10051007
context.state.template.push(statement);
10061008
} else {
10071009
context.state.template.push(block_open, statement, block_close);
@@ -1112,7 +1114,7 @@ const template_visitors = {
11121114
const parent = context.path.at(-1) ?? node;
11131115
const namespace = infer_namespace(context.state.namespace, parent, node.nodes);
11141116

1115-
const { hoisted, trimmed } = clean_nodes(
1117+
const { hoisted, trimmed, is_standalone } = clean_nodes(
11161118
parent,
11171119
node.nodes,
11181120
context.path,
@@ -1127,7 +1129,8 @@ const template_visitors = {
11271129
...context.state,
11281130
init: [],
11291131
template: [],
1130-
namespace
1132+
namespace,
1133+
skip_hydration_boundaries: is_standalone
11311134
};
11321135

11331136
for (const node of hoisted) {
@@ -1180,17 +1183,23 @@ const template_visitors = {
11801183
return /** @type {import('estree').Expression} */ (context.visit(arg));
11811184
});
11821185

1186+
if (!context.state.skip_hydration_boundaries) {
1187+
context.state.template.push(block_open);
1188+
}
1189+
11831190
context.state.template.push(
1184-
block_open,
11851191
b.stmt(
11861192
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
11871193
snippet_function,
11881194
b.id('$$payload'),
11891195
...snippet_args
11901196
)
1191-
),
1192-
block_close
1197+
)
11931198
);
1199+
1200+
if (!context.state.skip_hydration_boundaries) {
1201+
context.state.template.push(block_close);
1202+
}
11941203
},
11951204
ClassDirective() {
11961205
throw new Error('Node should have been handled elsewhere');
@@ -1925,7 +1934,8 @@ export function server_component(analysis, options) {
19251934
template: /** @type {any} */ (null),
19261935
namespace: options.namespace,
19271936
preserve_whitespace: options.preserveWhitespace,
1928-
private_derived: new Map()
1937+
private_derived: new Map(),
1938+
skip_hydration_boundaries: false
19291939
};
19301940

19311941
const module = /** @type {import('estree').Program} */ (

‎packages/svelte/src/compiler/phases/3-transform/server/types.d.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface ComponentServerTransformState extends ServerTransformState {
2222
readonly template: Array<Statement | Expression>;
2323
readonly namespace: Namespace;
2424
readonly preserve_whitespace: boolean;
25+
readonly skip_hydration_boundaries: boolean;
2526
}
2627

2728
export type Context = import('zimmerframe').Context<SvelteNode, ServerTransformState>;

‎packages/svelte/src/compiler/phases/3-transform/utils.js‎

Lines changed: 87 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -185,83 +185,105 @@ export function clean_nodes(
185185
}
186186
}
187187

188-
if (preserve_whitespace) {
189-
return { hoisted, trimmed: regular };
190-
}
188+
let trimmed = regular;
191189

192-
let first, last;
190+
if (!preserve_whitespace) {
191+
trimmed = [];
193192

194-
while ((first = regular[0]) && first.type === 'Text' && !regex_not_whitespace.test(first.data)) {
195-
regular.shift();
196-
}
193+
let first, last;
197194

198-
if (first?.type === 'Text') {
199-
first.raw = first.raw.replace(regex_starts_with_whitespaces, '');
200-
first.data = first.data.replace(regex_starts_with_whitespaces, '');
201-
}
195+
while (
196+
(first = regular[0]) &&
197+
first.type === 'Text' &&
198+
!regex_not_whitespace.test(first.data)
199+
) {
200+
regular.shift();
201+
}
202202

203-
while ((last = regular.at(-1)) && last.type === 'Text' && !regex_not_whitespace.test(last.data)) {
204-
regular.pop();
205-
}
203+
if (first?.type === 'Text') {
204+
first.raw = first.raw.replace(regex_starts_with_whitespaces, '');
205+
first.data = first.data.replace(regex_starts_with_whitespaces, '');
206+
}
206207

207-
if (last?.type === 'Text') {
208-
last.raw = last.raw.replace(regex_ends_with_whitespaces, '');
209-
last.data = last.data.replace(regex_ends_with_whitespaces, '');
210-
}
208+
while (
209+
(last = regular.at(-1)) &&
210+
last.type === 'Text' &&
211+
!regex_not_whitespace.test(last.data)
212+
) {
213+
regular.pop();
214+
}
211215

212-
const can_remove_entirely =
213-
(namespace === 'svg' &&
214-
(parent.type !== 'RegularElement' || parent.name !== 'text') &&
215-
!path.some((n) => n.type === 'RegularElement' && n.name === 'text')) ||
216-
(parent.type === 'RegularElement' &&
217-
// TODO others?
218-
(parent.name === 'select' ||
219-
parent.name === 'tr' ||
220-
parent.name === 'table' ||
221-
parent.name === 'tbody' ||
222-
parent.name === 'thead' ||
223-
parent.name === 'tfoot' ||
224-
parent.name === 'colgroup' ||
225-
parent.name === 'datalist'));
216+
if (last?.type === 'Text') {
217+
last.raw = last.raw.replace(regex_ends_with_whitespaces, '');
218+
last.data = last.data.replace(regex_ends_with_whitespaces, '');
219+
}
226220

227-
/** @type {Compiler.SvelteNode[]} */
228-
const trimmed = [];
229-
230-
// Replace any whitespace between a text and non-text node with a single spaceand keep whitespace
231-
// as-is within text nodes, or between text nodes and expression tags (because in the end they count
232-
// as one text). This way whitespace is mostly preserved when using CSS with `white-space: pre-line`
233-
// and default slot content going into a pre tag (which we can't see).
234-
for (let i = 0; i < regular.length; i++) {
235-
const prev = regular[i - 1];
236-
const node = regular[i];
237-
const next = regular[i + 1];
238-
239-
if (node.type === 'Text') {
240-
if (prev?.type !== 'ExpressionTag') {
241-
const prev_is_text_ending_with_whitespace =
242-
prev?.type === 'Text' && regex_ends_with_whitespaces.test(prev.data);
243-
node.data = node.data.replace(
244-
regex_starts_with_whitespaces,
245-
prev_is_text_ending_with_whitespace ? '' : ' '
246-
);
247-
node.raw = node.raw.replace(
248-
regex_starts_with_whitespaces,
249-
prev_is_text_ending_with_whitespace ? '' : ' '
250-
);
251-
}
252-
if (next?.type !== 'ExpressionTag') {
253-
node.data = node.data.replace(regex_ends_with_whitespaces, ' ');
254-
node.raw = node.raw.replace(regex_ends_with_whitespaces, ' ');
255-
}
256-
if (node.data && (node.data !== ' ' || !can_remove_entirely)) {
221+
const can_remove_entirely =
222+
(namespace === 'svg' &&
223+
(parent.type !== 'RegularElement' || parent.name !== 'text') &&
224+
!path.some((n) => n.type === 'RegularElement' && n.name === 'text')) ||
225+
(parent.type === 'RegularElement' &&
226+
// TODO others?
227+
(parent.name === 'select' ||
228+
parent.name === 'tr' ||
229+
parent.name === 'table' ||
230+
parent.name === 'tbody' ||
231+
parent.name === 'thead' ||
232+
parent.name === 'tfoot' ||
233+
parent.name === 'colgroup' ||
234+
parent.name === 'datalist'));
235+
236+
// Replace any whitespace between a text and non-text node with a single spaceand keep whitespace
237+
// as-is within text nodes, or between text nodes and expression tags (because in the end they count
238+
// as one text). This way whitespace is mostly preserved when using CSS with `white-space: pre-line`
239+
// and default slot content going into a pre tag (which we can't see).
240+
for (let i = 0; i < regular.length; i++) {
241+
const prev = regular[i - 1];
242+
const node = regular[i];
243+
const next = regular[i + 1];
244+
245+
if (node.type === 'Text') {
246+
if (prev?.type !== 'ExpressionTag') {
247+
const prev_is_text_ending_with_whitespace =
248+
prev?.type === 'Text' && regex_ends_with_whitespaces.test(prev.data);
249+
node.data = node.data.replace(
250+
regex_starts_with_whitespaces,
251+
prev_is_text_ending_with_whitespace ? '' : ' '
252+
);
253+
node.raw = node.raw.replace(
254+
regex_starts_with_whitespaces,
255+
prev_is_text_ending_with_whitespace ? '' : ' '
256+
);
257+
}
258+
if (next?.type !== 'ExpressionTag') {
259+
node.data = node.data.replace(regex_ends_with_whitespaces, ' ');
260+
node.raw = node.raw.replace(regex_ends_with_whitespaces, ' ');
261+
}
262+
if (node.data && (node.data !== ' ' || !can_remove_entirely)) {
263+
trimmed.push(node);
264+
}
265+
} else {
257266
trimmed.push(node);
258267
}
259-
} else {
260-
trimmed.push(node);
261268
}
262269
}
263270

264-
return { hoisted, trimmed };
271+
var first = trimmed[0];
272+
273+
/**
274+
* In a case like `{#if x}<Foo />{/if}`, we don't need to wrap the child in
275+
* comments — we can just use the parent block's anchor for the component.
276+
* TODO extend this optimisation to other cases
277+
*/
278+
const is_standalone =
279+
trimmed.length === 1 &&
280+
((first.type === 'RenderTag' && !first.metadata.dynamic) ||
281+
(first.type === 'Component' &&
282+
!first.attributes.some(
283+
(attribute) => attribute.type === 'Attribute' && attribute.name.startsWith('--')
284+
)));
285+
286+
return { hoisted, trimmed, is_standalone };
265287
}
266288

267289
/**

‎packages/svelte/src/constants.js‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export const TRANSITION_GLOBAL = 1 << 2;
1818

1919
export const TEMPLATE_FRAGMENT = 1;
2020
export const TEMPLATE_USE_IMPORT_NODE = 1 << 1;
21-
export const TEMPLATE_UNSET_START = 1 << 2;
2221

2322
export const HYDRATION_START = '[';
2423
export const HYDRATION_END = ']';

‎packages/svelte/src/internal/client/dev/hmr.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/** @import { Source, Effect } from '#client' */
2+
import { empty } from '../dom/operations.js';
23
import { block, branch, destroy_effect } from '../reactivity/effects.js';
34
import { set_should_intro } from '../render.js';
45
import { get } from '../runtime.js';
@@ -19,7 +20,7 @@ export function hmr(source) {
1920
/** @type {Effect} */
2021
let effect;
2122

22-
block(anchor, 0, () => {
23+
block(() => {
2324
const component = get(source);
2425

2526
if (effect) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
105105
}
106106
}
107107

108-
var effect = block(anchor, 0, () => {
108+
var effect = block(() => {
109109
if (input === (input = get_input())) return;
110110

111111
if (is_promise(input)) {

‎packages/svelte/src/internal/client/dom/blocks/each.js‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ import {
2424
run_out_transitions,
2525
pause_children,
2626
pause_effect,
27-
resume_effect,
28-
get_first_node
27+
resume_effect
2928
} from '../../reactivity/effects.js';
3029
import { source, mutable_source, set } from '../../reactivity/sources.js';
3130
import { is_array, is_frozen } from '../../utils.js';
@@ -124,7 +123,7 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
124123
/** @type {import('#client').Effect | null} */
125124
var fallback = null;
126125

127-
block(anchor, 0, () => {
126+
block(() => {
128127
var collection = get_collection();
129128

130129
var array = is_array(collection)
@@ -294,7 +293,9 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
294293
item = items.get(key);
295294

296295
if (item === undefined) {
297-
var child_anchor = current ? get_first_node(current.e) : anchor;
296+
var child_anchor = current
297+
? /** @type {import('#client').EffectNodes} */ (current.e.nodes).start
298+
: anchor;
298299

299300
prev = create_item(
300301
child_anchor,
@@ -515,10 +516,12 @@ function create_item(anchor, state, prev, next, value, key, index, render_fn, fl
515516
* @param {Text | Element | Comment} anchor
516517
*/
517518
function move(item, next, anchor) {
518-
var end = item.next ? get_first_node(item.next.e) : anchor;
519-
var dest = next ? get_first_node(next.e) : anchor;
519+
var end = item.next
520+
? /** @type {import('#client').EffectNodes} */ (item.next.e.nodes).start
521+
: anchor;
520522

521-
var node = get_first_node(item.e);
523+
var dest = next ? /** @type {import('#client').EffectNodes} */ (next.e.nodes).start : anchor;
524+
var node = /** @type {import('#client').EffectNodes} */ (item.e.nodes).start;
522525

523526
while (node !== end) {
524527
var next_node = /** @type {import('#client').TemplateNode} */ (node.nextSibling);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function html(anchor, get_value, svg, mathml) {
1616
/** @type {import('#client').Effect | null} */
1717
var effect;
1818

19-
block(anchor, 0, () => {
19+
block(() => {
2020
if (value === (value = get_value())) return;
2121

2222
if (effect) {

‎packages/svelte/src/internal/client/dom/blocks/if.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export function if_block(
3030

3131
var flags = elseif ? EFFECT_TRANSPARENT : 0;
3232

33-
block(anchor, flags, () => {
33+
block(() => {
3434
if (condition === (condition = !!get_condition())) return;
3535

3636
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
@@ -78,5 +78,5 @@ export function if_block(
7878
// continue in hydration mode
7979
set_hydrating(true);
8080
}
81-
});
81+
}, flags);
8282
}

‎packages/svelte/src/internal/client/dom/blocks/key.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function key_block(anchor, get_key, render_fn) {
1717
/** @type {Effect} */
1818
let effect;
1919

20-
block(anchor, 0, () => {
20+
block(() => {
2121
if (safe_not_equal(key, (key = get_key()))) {
2222
if (effect) {
2323
pause_effect(effect);

‎packages/svelte/src/internal/client/dom/blocks/snippet.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function snippet(anchor, get_snippet, ...args) {
2020
/** @type {import('#client').Effect | null} */
2121
var snippet_effect;
2222

23-
block(anchor, EFFECT_TRANSPARENT, () => {
23+
block(() => {
2424
if (snippet === (snippet = get_snippet())) return;
2525

2626
if (snippet_effect) {
@@ -31,7 +31,7 @@ export function snippet(anchor, get_snippet, ...args) {
3131
if (snippet) {
3232
snippet_effect = branch(() => /** @type {SnippetFn} */ (snippet)(anchor, ...args));
3333
}
34-
});
34+
}, EFFECT_TRANSPARENT);
3535
}
3636

3737
/**
Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
/** @import { TemplateNode, Dom, Effect } from '#client' */
2-
import { DEV } from 'esm-env';
32
import { block, branch, pause_effect } from '../../reactivity/effects.js';
4-
import { empty } from '../operations.js';
53

64
/**
75
* @template P
@@ -18,12 +16,7 @@ export function component(anchor, get_component, render_fn) {
1816
/** @type {Effect | null} */
1917
let effect;
2018

21-
var component_anchor = anchor;
22-
23-
// create a dummy anchor for the HMR wrapper, if such there be
24-
if (DEV) component_anchor = empty();
25-
26-
block(anchor, 0, () => {
19+
block(() => {
2720
if (component === (component = get_component())) return;
2821

2922
if (effect) {
@@ -32,8 +25,7 @@ export function component(anchor, get_component, render_fn) {
3225
}
3326

3427
if (component) {
35-
if (DEV) anchor.before(component_anchor);
36-
effect = branch(() => render_fn(component_anchor, component));
28+
effect = branch(() => render_fn(anchor, component));
3729
}
3830
});
3931
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
4848
*/
4949
let each_item_block = current_each_item;
5050

51-
block(anchor, 0, () => {
51+
block(() => {
5252
const next_tag = get_tag() || null;
5353
const ns = get_namespace
5454
? get_namespace()

‎packages/svelte/src/internal/client/dom/blocks/svelte-head.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function head(render_fn) {
4848
}
4949

5050
try {
51-
block(null, HEAD_EFFECT, () => render_fn(anchor));
51+
block(() => render_fn(anchor), HEAD_EFFECT);
5252
} finally {
5353
if (was_hydrating) {
5454
set_hydrate_nodes(/** @type {import('#client').TemplateNode[]} */ (previous_hydrate_nodes));

‎packages/svelte/src/internal/client/dom/operations.js‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,12 @@ export function child(node) {
7878
export function first_child(fragment, is_text) {
7979
if (!hydrating) {
8080
// when not hydrating, `fragment` is a `DocumentFragment` (the result of calling `open_frag`)
81-
return /** @type {DocumentFragment} */ (fragment).firstChild;
81+
var first = /** @type {DocumentFragment} */ (fragment).firstChild;
82+
83+
// TODO prevent user comments with the empty string when preserveComments is true
84+
if (first instanceof Comment && first.data === '') return first.nextSibling;
85+
86+
return first;
8287
}
8388

8489
// if an {expression} is empty during SSR, there might be no

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

Lines changed: 24 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,15 @@ import { get_start, hydrate_nodes, hydrate_start, hydrating } from './hydration.
22
import { empty } from './operations.js';
33
import { create_fragment_from_html } from './reconciler.js';
44
import { current_effect } from '../runtime.js';
5-
import {
6-
TEMPLATE_FRAGMENT,
7-
TEMPLATE_UNSET_START,
8-
TEMPLATE_USE_IMPORT_NODE
9-
} from '../../../constants.js';
5+
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
106
import { queue_micro_task } from './task.js';
117

128
/**
13-
*
14-
* @param {import('#client').TemplateNode | undefined | null} start
9+
* @param {import('#client').TemplateNode} start
1510
* @param {import('#client').TemplateNode} end
16-
* @param {import('#client').TemplateNode | null} anchor
1711
*/
18-
export function assign_nodes(start, end, anchor = null) {
19-
const effect = /** @type {import('#client').Effect} */ (current_effect);
20-
21-
if (effect.nodes === null) {
22-
effect.nodes = { start, anchor, end };
23-
} else if (effect.nodes.start === undefined) {
24-
effect.nodes.start = start;
25-
}
12+
export function assign_nodes(start, end) {
13+
/** @type {import('#client').Effect} */ (current_effect).nodes ??= { start, end };
2614
}
2715

2816
/**
@@ -38,8 +26,11 @@ export function template(content, flags) {
3826
/** @type {Node} */
3927
var node;
4028

29+
/**
30+
* Whether or not the first item is a text/element node. If not, we need to
31+
* create an additional comment node to act as `effect.nodes.start`
32+
*/
4133
var has_start = !content.startsWith('<!>');
42-
var unset = (flags & TEMPLATE_UNSET_START) !== 0;
4334

4435
return () => {
4536
if (hydrating) {
@@ -49,7 +40,7 @@ export function template(content, flags) {
4940
}
5041

5142
if (!node) {
52-
node = create_fragment_from_html(content);
43+
node = create_fragment_from_html(has_start ? content : '<!>' + content);
5344
if (!is_fragment) node = /** @type {Node} */ (node.firstChild);
5445
}
5546

@@ -58,11 +49,10 @@ export function template(content, flags) {
5849
);
5950

6051
if (is_fragment) {
61-
var first = /** @type {import('#client').TemplateNode} */ (clone.firstChild);
62-
var start = has_start ? first : unset ? undefined : null;
52+
var start = /** @type {import('#client').TemplateNode} */ (clone.firstChild);
6353
var end = /** @type {import('#client').TemplateNode} */ (clone.lastChild);
6454

65-
assign_nodes(start, end, first);
55+
assign_nodes(start, end);
6656
} else {
6757
assign_nodes(clone, clone);
6858
}
@@ -103,15 +93,18 @@ export function template_with_script(content, flags) {
10393
*/
10494
/*#__NO_SIDE_EFFECTS__*/
10595
export function ns_template(content, flags, ns = 'svg') {
96+
/**
97+
* Whether or not the first item is a text/element node. If not, we need to
98+
* create an additional comment node to act as `effect.nodes.start`
99+
*/
100+
var has_start = !content.startsWith('<!>');
101+
106102
var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0;
107-
var wrapped = `<${ns}>${content}</${ns}>`;
103+
var wrapped = `<${ns}>${has_start ? content : '<!>' + content}</${ns}>`;
108104

109105
/** @type {Element | DocumentFragment} */
110106
var node;
111107

112-
var has_start = !content.startsWith('<!>');
113-
var unset = (flags & TEMPLATE_UNSET_START) !== 0;
114-
115108
return () => {
116109
if (hydrating) {
117110
assign_nodes(get_start(), hydrate_nodes[hydrate_nodes.length - 1]);
@@ -136,11 +129,10 @@ export function ns_template(content, flags, ns = 'svg') {
136129
var clone = /** @type {import('#client').TemplateNode} */ (node.cloneNode(true));
137130

138131
if (is_fragment) {
139-
var first = /** @type {import('#client').TemplateNode} */ (clone.firstChild);
140-
var start = has_start ? first : unset ? undefined : null;
132+
var start = /** @type {import('#client').TemplateNode} */ (clone.firstChild);
141133
var end = /** @type {import('#client').TemplateNode} */ (clone.lastChild);
142134

143-
assign_nodes(start, end, first);
135+
assign_nodes(start, end);
144136
} else {
145137
assign_nodes(clone, clone);
146138
}
@@ -238,10 +230,7 @@ export function text(anchor) {
238230
return node;
239231
}
240232

241-
/**
242-
* @param {boolean} unset
243-
*/
244-
export function comment(unset = false) {
233+
export function comment() {
245234
// we're not delegating to `template` here for performance reasons
246235
if (hydrating) {
247236
assign_nodes(get_start(), hydrate_nodes[hydrate_nodes.length - 1]);
@@ -250,10 +239,11 @@ export function comment(unset = false) {
250239
}
251240

252241
var frag = document.createDocumentFragment();
242+
var start = document.createComment('');
253243
var anchor = empty();
254-
frag.append(anchor);
244+
frag.append(start, anchor);
255245

256-
assign_nodes(unset ? undefined : null, anchor, anchor);
246+
assign_nodes(start, anchor);
257247

258248
return frag;
259249
}

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

Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -299,14 +299,11 @@ export function template_effect(fn) {
299299
}
300300

301301
/**
302-
* @param {import('#client').TemplateNode | null} anchor
303-
* @param {number} flags
304302
* @param {(() => void)} fn
303+
* @param {number} flags
305304
*/
306-
export function block(anchor, flags, fn) {
307-
const effect = create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true);
308-
if (anchor !== null) effect.nodes = { start: null, anchor: null, end: anchor };
309-
return effect;
305+
export function block(fn, flags = 0) {
306+
return create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true);
310307
}
311308

312309
/**
@@ -346,7 +343,7 @@ export function destroy_effect(effect, remove_dom = true) {
346343

347344
if ((remove_dom || (effect.f & HEAD_EFFECT) !== 0) && effect.nodes !== null) {
348345
/** @type {import('#client').TemplateNode | null} */
349-
var node = get_first_node(effect);
346+
var node = effect.nodes.start;
350347
var end = effect.nodes.end;
351348

352349
while (node !== null) {
@@ -392,37 +389,6 @@ export function destroy_effect(effect, remove_dom = true) {
392389
null;
393390
}
394391

395-
/**
396-
* @param {import('#client').Effect} effect
397-
* @returns {import('#client').TemplateNode}
398-
*/
399-
export function get_first_node(effect) {
400-
var nodes = /** @type {NonNullable<typeof effect.nodes>} */ (effect.nodes);
401-
var start = nodes.start;
402-
403-
if (start === undefined) {
404-
// edge case — a snippet or component was the first item inside the effect,
405-
// but it didn't render any DOM. in this case, we return the item's anchor
406-
return /** @type {import('#client').TemplateNode} */ (nodes.anchor);
407-
}
408-
409-
if (start !== null) {
410-
return start;
411-
}
412-
413-
var child = effect.first;
414-
while (child && (child.nodes === null || (child.f & HEAD_EFFECT) !== 0)) {
415-
child = child.next;
416-
}
417-
418-
if (child !== null && child.nodes !== null) {
419-
return get_first_node(child);
420-
}
421-
422-
// in the case that there's no DOM, return the first anchor
423-
return nodes.end;
424-
}
425-
426392
/**
427393
* Detach an effect from the effect tree, freeing up memory and
428394
* reducing the amount of work that happens on subsequent traversals

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,20 @@ export interface Derived<V = unknown> extends Value<V>, Reaction {
3434
deriveds: null | Derived[];
3535
}
3636

37+
export interface EffectNodes {
38+
start: TemplateNode;
39+
end: TemplateNode;
40+
}
41+
3742
export interface Effect extends Reaction {
3843
parent: Effect | null;
39-
nodes: null | {
40-
start: undefined | null | TemplateNode;
41-
anchor: null | TemplateNode;
42-
end: TemplateNode;
43-
};
44+
/**
45+
* Branch effects store their start/end nodes so that they can be
46+
* removed when the effect is destroyed, or moved when an `each`
47+
* block is reconciled. In the case of a single text/element node,
48+
* `start` and `end` will be the same.
49+
*/
50+
nodes: null | EffectNodes;
4451
/** The associated component context */
4552
ctx: null | ComponentContext;
4653
/** The effect function */

‎packages/svelte/src/internal/client/render.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { DEV } from 'esm-env';
22
import { clear_text_content, create_element, empty, init_operations } from './dom/operations.js';
33
import { HYDRATION_ERROR, HYDRATION_START, PassiveDelegatedEvents } from '../../constants.js';
4-
import { flush_sync, push, pop, current_component_context } from './runtime.js';
4+
import { flush_sync, push, pop, current_component_context, current_effect } from './runtime.js';
55
import { effect_root, branch } from './reactivity/effects.js';
66
import {
77
hydrate_anchor,

‎packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as $ from "svelte/internal/client";
33
import TextInput from './Child.svelte';
44

55
var root_1 = $.template(`Something`, 1);
6-
var root = $.template(`<!> `, 5);
6+
var root = $.template(`<!> `, 1);
77

88
export default function Bind_component_snippet($$anchor) {
99
const snippet = ($$anchor) => {

‎packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,5 @@ import "svelte/internal/disclose-version";
22
import * as $ from "svelte/internal/client";
33

44
export default function Bind_this($$anchor) {
5-
var fragment = $.comment(true);
6-
var node = $.first_child(fragment);
7-
8-
$.bind_this(Foo(node, { $$legacy: true }), ($$value) => foo = $$value, () => foo);
9-
$.append($$anchor, fragment);
5+
$.bind_this(Foo($$anchor, { $$legacy: true }), ($$value) => foo = $$value, () => foo);
106
}
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import * as $ from "svelte/internal/server";
22

33
export default function Bind_this($$payload) {
4-
$$payload.out += `<!--[-->`;
54
Foo($$payload, {});
6-
$$payload.out += `<!--]-->`;
75
}

‎packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@ export default function Function_prop_no_getter($$anchor) {
99
}
1010

1111
const plusOne = (num) => num + 1;
12-
var fragment = $.comment(true);
13-
var node = $.first_child(fragment);
1412

15-
Button(node, {
13+
Button($$anchor, {
1614
onmousedown: () => $.set(count, $.get(count) + 1),
1715
onmouseup,
1816
onmouseenter: () => $.set(count, $.proxy(plusOne($.get(count)))),
@@ -24,6 +22,4 @@ export default function Function_prop_no_getter($$anchor) {
2422
},
2523
$$slots: { default: true }
2624
});
27-
28-
$.append($$anchor, fragment);
2925
}

‎packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ export default function Function_prop_no_getter($$payload) {
99

1010
const plusOne = (num) => num + 1;
1111

12-
$$payload.out += `<!--[-->`;
13-
1412
Button($$payload, {
1513
onmousedown: () => count += 1,
1614
onmouseup,
@@ -20,6 +18,4 @@ export default function Function_prop_no_getter($$payload) {
2018
},
2119
$$slots: { default: true }
2220
});
23-
24-
$$payload.out += `<!--]-->`;
2521
}

0 commit comments

Comments
 (0)
Please sign in to comment.