Skip to content

Commit dfd1819

Browse files
authored
chore: tidy up hydration code (#10891)
* remove some indirection * tidy up * tidy * tidy up * simplify * fix * don't attempt to hydrate children of void dynamic element * simplify * tighten up * fix * add note, simplify * tidy up * changeset * revert this change, save for a separate PR
1 parent 7f10642 commit dfd1819

File tree

10 files changed

+174
-174
lines changed

10 files changed

+174
-174
lines changed

packages/svelte/src/internal/client/dom/blocks/css-props.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { namespace_svg } from '../../../../constants.js';
2-
import { current_hydration_fragment, hydrate_block_anchor, hydrating } from '../hydration.js';
2+
import { hydrate_nodes, hydrate_block_anchor, hydrating } from '../hydration.js';
33
import { empty } from '../operations.js';
44
import { render_effect } from '../../reactivity/effects.js';
55
import { remove } from '../reconciler.js';
@@ -22,7 +22,7 @@ export function css_props(anchor, is_html, props, component) {
2222

2323
if (hydrating) {
2424
// Hydration: css props element is surrounded by a ssr comment ...
25-
tag = /** @type {HTMLElement | SVGElement} */ (current_hydration_fragment[0]);
25+
tag = /** @type {HTMLElement | SVGElement} */ (hydrate_nodes[0]);
2626
// ... and the child(ren) of the css props element is also surround by a ssr comment
2727
component_anchor = /** @type {Comment} */ (tag.firstChild);
2828
} else {

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

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import {
77
EACH_KEYED
88
} from '../../../../constants.js';
99
import {
10-
current_hydration_fragment,
11-
get_hydration_fragment,
10+
hydrate_nodes,
1211
hydrate_block_anchor,
1312
hydrating,
14-
set_current_hydration_fragment
13+
set_hydrating,
14+
update_hydrate_nodes
1515
} from '../hydration.js';
1616
import { empty } from '../operations.js';
1717
import { insert, remove } from '../reconciler.js';
@@ -98,17 +98,16 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re
9898
let mismatch = false;
9999

100100
if (hydrating) {
101-
var is_else =
102-
/** @type {Comment} */ (current_hydration_fragment?.[0])?.data === 'ssr:each_else';
101+
var is_else = /** @type {Comment} */ (hydrate_nodes?.[0])?.data === 'ssr:each_else';
103102

104103
if (is_else !== (length === 0)) {
105104
// hydration mismatch — remove the server-rendered DOM and start over
106-
remove(current_hydration_fragment);
107-
set_current_hydration_fragment(null);
105+
remove(hydrate_nodes);
106+
set_hydrating(false);
108107
mismatch = true;
109108
} else if (is_else) {
110109
// Remove the each_else comment node or else it will confuse the subsequent hydration algorithm
111-
/** @type {import('#client').TemplateNode[]} */ (current_hydration_fragment).shift();
110+
/** @type {import('#client').TemplateNode[]} */ (hydrate_nodes).shift();
112111
}
113112
}
114113

@@ -117,18 +116,17 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re
117116
var b_items = [];
118117

119118
// Hydrate block
120-
var hydration_list = /** @type {import('#client').TemplateNode[]} */ (
121-
current_hydration_fragment
122-
);
119+
var hydration_list = /** @type {import('#client').TemplateNode[]} */ (hydrate_nodes);
123120
var hydrating_node = hydration_list[0];
124121

125122
for (var i = 0; i < length; i++) {
126-
var fragment = get_hydration_fragment(hydrating_node);
127-
set_current_hydration_fragment(fragment);
128-
if (!fragment) {
129-
// If fragment is null, then that means that the server rendered less items than what
130-
// the client code specifies -> break out and continue with client-side node creation
123+
var nodes = update_hydrate_nodes(hydrating_node);
124+
125+
if (nodes === null) {
126+
// If `nodes` is null, then that means that the server rendered fewer items than what
127+
// expected, so break out and continue appending non-hydrated items
131128
mismatch = true;
129+
set_hydrating(false);
132130
break;
133131
}
134132

@@ -137,7 +135,7 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re
137135
// TODO helperise this
138136
hydrating_node = /** @type {import('#client').TemplateNode} */ (
139137
/** @type {Node} */ (
140-
/** @type {Node} */ (fragment[fragment.length - 1] || hydrating_node).nextSibling
138+
/** @type {Node} */ (nodes[nodes.length - 1] || hydrating_node).nextSibling
141139
).nextSibling
142140
);
143141
}
@@ -175,8 +173,8 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re
175173
}
176174

177175
if (mismatch) {
178-
// Set a fragment so that Svelte continues to operate in hydration mode
179-
set_current_hydration_fragment([]);
176+
// continue in hydration mode
177+
set_hydrating(true);
180178
}
181179
});
182180

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

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import { IS_ELSEIF } from '../../constants.js';
2-
import {
3-
current_hydration_fragment,
4-
hydrate_block_anchor,
5-
hydrating,
6-
set_current_hydration_fragment
7-
} from '../hydration.js';
2+
import { hydrate_nodes, hydrate_block_anchor, hydrating, set_hydrating } from '../hydration.js';
83
import { remove } from '../reconciler.js';
94
import {
105
destroy_effect,
@@ -40,7 +35,7 @@ export function if_block(anchor, get_condition, consequent_fn, alternate_fn, els
4035
let mismatch = false;
4136

4237
if (hydrating) {
43-
const comment_text = /** @type {Comment} */ (current_hydration_fragment?.[0])?.data;
38+
const comment_text = /** @type {Comment} */ (hydrate_nodes?.[0])?.data;
4439

4540
if (
4641
!comment_text ||
@@ -49,12 +44,12 @@ export function if_block(anchor, get_condition, consequent_fn, alternate_fn, els
4944
) {
5045
// Hydration mismatch: remove everything inside the anchor and start fresh.
5146
// This could happen using when `{#if browser} .. {/if}` in SvelteKit.
52-
remove(current_hydration_fragment);
53-
set_current_hydration_fragment(null);
47+
remove(hydrate_nodes);
48+
set_hydrating(false);
5449
mismatch = true;
5550
} else {
5651
// Remove the ssr:if comment node or else it will confuse the subsequent hydration algorithm
57-
current_hydration_fragment.shift();
52+
hydrate_nodes.shift();
5853
}
5954
}
6055

@@ -85,8 +80,8 @@ export function if_block(anchor, get_condition, consequent_fn, alternate_fn, els
8580
}
8681

8782
if (mismatch) {
88-
// Set fragment so that Svelte continues to operate in hydration mode
89-
set_current_hydration_fragment([]);
83+
// continue in hydration mode
84+
set_hydrating(true);
9085
}
9186
});
9287

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

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { namespace_svg } from '../../../../constants.js';
2-
import { current_hydration_fragment, hydrate_block_anchor, hydrating } from '../hydration.js';
2+
import { hydrate_nodes, hydrate_block_anchor, hydrating } from '../hydration.js';
33
import { empty } from '../operations.js';
44
import {
55
destroy_effect,
@@ -105,22 +105,25 @@ export function element(anchor, get_tag, is_svg, render_fn) {
105105
effect = render_effect(() => {
106106
const prev_element = element;
107107
element = hydrating
108-
? /** @type {Element} */ (current_hydration_fragment[0])
108+
? /** @type {Element} */ (hydrate_nodes[0])
109109
: ns
110110
? document.createElementNS(ns, next_tag)
111111
: document.createElement(next_tag);
112112

113113
if (render_fn) {
114-
let anchor;
115-
if (hydrating) {
116-
// Use the existing ssr comment as the anchor so that the inner open and close
117-
// methods can pick up the existing nodes correctly
118-
anchor = /** @type {Comment} */ (element.firstChild);
119-
} else {
120-
anchor = empty();
121-
element.appendChild(anchor);
114+
// If hydrating, use the existing ssr comment as the anchor so that the
115+
// inner open and close methods can pick up the existing nodes correctly
116+
var child_anchor = hydrating
117+
? /** @type {Comment} */ (element.firstChild)
118+
: element.appendChild(empty());
119+
120+
if (child_anchor) {
121+
// `child_anchor` can be undefined if this is a void element with children,
122+
// i.e. `<svelte:element this={"hr"}>...</svelte:element>`. This is
123+
// user error, but we warn on it elsewhere (in dev) so here we just
124+
// silently ignore it
125+
render_fn(element, child_anchor);
122126
}
123-
render_fn(element, anchor);
124127
}
125128

126129
anchor.before(element);

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

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
import {
2-
current_hydration_fragment,
3-
get_hydration_fragment,
4-
hydrating,
5-
set_current_hydration_fragment
6-
} from '../hydration.js';
1+
import { hydrate_nodes, hydrating, set_hydrate_nodes, update_hydrate_nodes } from '../hydration.js';
72
import { empty } from '../operations.js';
83
import { render_effect } from '../../reactivity/effects.js';
94
import { remove } from '../reconciler.js';
@@ -15,14 +10,12 @@ import { remove } from '../reconciler.js';
1510
export function head(render_fn) {
1611
// The head function may be called after the first hydration pass and ssr comment nodes may still be present,
1712
// therefore we need to skip that when we detect that we're not in hydration mode.
18-
let hydration_fragment = null;
19-
let previous_hydration_fragment = null;
13+
let previous_hydrate_nodes = null;
14+
let was_hydrating = hydrating;
2015

21-
let is_hydrating = hydrating;
22-
if (is_hydrating) {
23-
hydration_fragment = get_hydration_fragment(document.head.firstChild);
24-
previous_hydration_fragment = current_hydration_fragment;
25-
set_current_hydration_fragment(hydration_fragment);
16+
if (hydrating) {
17+
previous_hydrate_nodes = hydrate_nodes;
18+
update_hydrate_nodes(document.head.firstChild);
2619
}
2720

2821
try {
@@ -50,8 +43,8 @@ export function head(render_fn) {
5043
}
5144
};
5245
} finally {
53-
if (is_hydrating) {
54-
set_current_hydration_fragment(previous_hydration_fragment);
46+
if (was_hydrating) {
47+
set_hydrate_nodes(previous_hydrate_nodes);
5548
}
5649
}
5750
}
Lines changed: 70 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
// Handle hydration
2-
31
import { schedule_task } from './task.js';
42
import { empty } from './operations.js';
53

@@ -9,66 +7,79 @@ import { empty } from './operations.js';
97
*/
108
export let hydrating = false;
119

10+
/** @param {boolean} value */
11+
export function set_hydrating(value) {
12+
hydrating = value;
13+
}
14+
1215
/**
1316
* Array of nodes to traverse for hydration. This will be null if we're not hydrating, but for
1417
* the sake of simplicity we're not going to use `null` checks everywhere and instead rely on
1518
* the `hydrating` flag to tell whether or not we're in hydration mode at which point this is set.
16-
* @type {import('../types.js').TemplateNode[]}
19+
* @type {import('#client').TemplateNode[]}
1720
*/
18-
export let current_hydration_fragment = /** @type {any} */ (null);
21+
export let hydrate_nodes = /** @type {any} */ (null);
1922

2023
/**
21-
* @param {null | import('../types.js').TemplateNode[]} fragment
24+
* @param {null | import('#client').TemplateNode[]} nodes
2225
* @returns {void}
2326
*/
24-
export function set_current_hydration_fragment(fragment) {
25-
hydrating = fragment !== null;
26-
current_hydration_fragment = /** @type {import('../types.js').TemplateNode[]} */ (fragment);
27+
export function set_hydrate_nodes(nodes) {
28+
hydrate_nodes = /** @type {import('#client').TemplateNode[]} */ (nodes);
29+
}
30+
31+
/**
32+
* @param {Node | null} first
33+
* @param {boolean} [insert_text] Whether to insert an empty text node if `nodes` is empty
34+
*/
35+
export function update_hydrate_nodes(first, insert_text) {
36+
const nodes = get_hydrate_nodes(first, insert_text);
37+
set_hydrate_nodes(nodes);
38+
return nodes;
2739
}
2840

2941
/**
3042
* Returns all nodes between the first `<!--ssr:...-->` comment tag pair encountered.
3143
* @param {Node | null} node
32-
* @param {boolean} [insert_text] Whether to insert an empty text node if the fragment is empty
33-
* @returns {import('../types.js').TemplateNode[] | null}
44+
* @param {boolean} [insert_text] Whether to insert an empty text node if `nodes` is empty
45+
* @returns {import('#client').TemplateNode[] | null}
3446
*/
35-
export function get_hydration_fragment(node, insert_text = false) {
36-
/** @type {import('../types.js').TemplateNode[]} */
37-
const fragment = [];
47+
function get_hydrate_nodes(node, insert_text = false) {
48+
/** @type {import('#client').TemplateNode[]} */
49+
var nodes = [];
3850

39-
/** @type {null | Node} */
40-
let current_node = node;
51+
var current_node = /** @type {null | import('#client').TemplateNode} */ (node);
4152

4253
/** @type {null | string} */
43-
let target_depth = null;
54+
var target_depth = null;
55+
4456
while (current_node !== null) {
45-
const node_type = current_node.nodeType;
46-
const next_sibling = current_node.nextSibling;
47-
if (node_type === 8) {
48-
const data = /** @type {Comment} */ (current_node).data;
57+
if (current_node.nodeType === 8) {
58+
var data = /** @type {Comment} */ (current_node).data;
59+
4960
if (data.startsWith('ssr:')) {
50-
const depth = data.slice(4);
61+
var depth = data.slice(4);
62+
5163
if (target_depth === null) {
5264
target_depth = depth;
5365
} else if (depth === target_depth) {
54-
if (insert_text && fragment.length === 0) {
55-
const text = empty();
56-
fragment.push(text);
57-
/** @type {Node} */ (current_node.parentNode).insertBefore(text, current_node);
66+
if (insert_text && nodes.length === 0) {
67+
var text = empty();
68+
nodes.push(text);
69+
current_node.before(text);
5870
}
59-
return fragment;
71+
return nodes;
6072
} else {
61-
fragment.push(/** @type {Text | Comment | Element} */ (current_node));
73+
nodes.push(current_node);
6274
}
63-
current_node = next_sibling;
64-
continue;
6575
}
76+
} else if (target_depth !== null) {
77+
nodes.push(current_node);
6678
}
67-
if (target_depth !== null) {
68-
fragment.push(/** @type {Text | Comment | Element} */ (current_node));
69-
}
70-
current_node = next_sibling;
79+
80+
current_node = /** @type {null | import('#client').TemplateNode} */ (current_node.nextSibling);
7181
}
82+
7283
return null;
7384
}
7485

@@ -81,18 +92,39 @@ export function hydrate_block_anchor(node) {
8192

8293
if (node.nodeType === 8) {
8394
// @ts-ignore
84-
let fragment = node.$$fragment;
85-
if (fragment === undefined) {
86-
fragment = get_hydration_fragment(node);
95+
let nodes = node.$$fragment;
96+
if (nodes === undefined) {
97+
nodes = get_hydrate_nodes(node);
8798
} else {
8899
schedule_task(() => {
89100
// @ts-expect-error clean up memory
90101
node.$$fragment = undefined;
91102
});
92103
}
93-
set_current_hydration_fragment(fragment);
104+
set_hydrate_nodes(nodes);
94105
} else {
95106
const first_child = /** @type {Element | null} */ (node.firstChild);
96-
set_current_hydration_fragment(first_child === null ? [] : [first_child]);
107+
set_hydrate_nodes(first_child === null ? [] : [first_child]);
108+
}
109+
}
110+
111+
/**
112+
* Expects to only be called in hydration mode
113+
* @param {Node} node
114+
* @returns {Node}
115+
*/
116+
export function capture_fragment_from_node(node) {
117+
if (
118+
node.nodeType === 8 &&
119+
/** @type {Comment} */ (node).data.startsWith('ssr:') &&
120+
hydrate_nodes[hydrate_nodes.length - 1] !== node
121+
) {
122+
const nodes = /** @type {Node[]} */ (get_hydrate_nodes(node));
123+
const last_child = nodes[nodes.length - 1] || node;
124+
const target = /** @type {Node} */ (last_child.nextSibling);
125+
// @ts-ignore
126+
target.$$fragment = nodes;
127+
return target;
97128
}
129+
return node;
98130
}

0 commit comments

Comments
 (0)