Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: tweak hydration implementation #11690

Closed
wants to merge 43 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
60d5c96
groundwork
Rich-Harris May 19, 2024
f6b7b82
use hydrate_start instead of hydrate_nodes[0]
Rich-Harris May 19, 2024
b96e16d
unused import
Rich-Harris May 19, 2024
54ac2b4
groundwork
Rich-Harris May 19, 2024
3128858
groundwork
Rich-Harris May 19, 2024
182a2c6
groundwork
Rich-Harris May 19, 2024
f264ad1
mostly working
Rich-Harris May 19, 2024
10a3df2
working
Rich-Harris May 20, 2024
b34f118
simplify
Rich-Harris May 20, 2024
af0262b
simplify
Rich-Harris May 20, 2024
ff629ff
simplify
Rich-Harris May 20, 2024
c11bb5a
remove unused code
Rich-Harris May 20, 2024
1980fa0
unnecessary
Rich-Harris May 20, 2024
4b43ca9
reduce indirection
Rich-Harris May 20, 2024
45ad5dd
DRY
Rich-Harris May 20, 2024
1ff28d4
tweak
Rich-Harris May 20, 2024
86b06d1
tweak
Rich-Harris May 20, 2024
db9c144
tidy
Rich-Harris May 20, 2024
378a917
remove some hydrate_nodes occurrences
Rich-Harris May 20, 2024
22b8696
tidy
Rich-Harris May 20, 2024
5262b40
get rid of hydrate_nodes completely
Rich-Harris May 20, 2024
66a1368
tidy
Rich-Harris May 20, 2024
6730fed
simplify
Rich-Harris May 20, 2024
a28532d
tidy up
Rich-Harris May 20, 2024
74b2163
merge main
Rich-Harris May 23, 2024
0f4189a
fix
Rich-Harris May 23, 2024
f9f7104
merge main
Rich-Harris May 23, 2024
4d00f2c
Merge branch 'main' into hydration-gc
Rich-Harris May 23, 2024
87ed6f5
merge main
Rich-Harris May 23, 2024
699b63e
merge main
Rich-Harris May 23, 2024
edf499a
merge main
Rich-Harris May 23, 2024
d747025
merge main
Rich-Harris May 23, 2024
49c9324
unnecessary
Rich-Harris May 23, 2024
b9bc7c2
Merge branch 'main' into hydration-gc
Rich-Harris May 23, 2024
becd30f
merge main
Rich-Harris May 23, 2024
8396b51
Merge branch 'main' into hydration-gc
Rich-Harris May 24, 2024
f38ff54
merge main
Rich-Harris May 24, 2024
ced38b5
merge main
Rich-Harris May 24, 2024
9adc60e
tweak
Rich-Harris May 24, 2024
3cdca10
set d2 in append
Rich-Harris May 24, 2024
d9f9755
remove hydrate_end
Rich-Harris May 24, 2024
f5460e2
tweak
Rich-Harris May 24, 2024
04415e9
tweak
Rich-Harris May 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions packages/svelte/src/internal/client/dom/blocks/each.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ import {
} from '../../../../constants.js';
import {
hydrate_anchor,
hydrate_nodes,
hydrate_start,
hydrating,
remove_hydrate_nodes,
set_hydrating
} from '../hydration.js';
import { clear_text_content, empty } from '../operations.js';
import { remove } from '../reconciler.js';
import { untrack } from '../../runtime.js';
import {
block,
Expand Down Expand Up @@ -145,7 +144,7 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback

if (is_else !== (length === 0)) {
// hydration mismatch — remove the server-rendered DOM and start over
remove(hydrate_nodes);
remove_hydrate_nodes();
set_hydrating(false);
mismatch = true;
}
Expand Down
55 changes: 16 additions & 39 deletions packages/svelte/src/internal/client/dom/blocks/html.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,9 @@
import { derived } from '../../reactivity/deriveds.js';
import { render_effect } from '../../reactivity/effects.js';
import { current_effect, get } from '../../runtime.js';
import { is_array } from '../../utils.js';
import { hydrate_nodes, hydrating } from '../hydration.js';
import { create_fragment_from_html, remove } from '../reconciler.js';

/**
* @param {import('#client').Effect} effect
* @param {(Element | Comment | Text)[]} to_remove
* @returns {void}
*/
function remove_from_parent_effect(effect, to_remove) {
const dom = effect.dom;

if (is_array(dom)) {
for (let i = dom.length - 1; i >= 0; i--) {
if (to_remove.includes(dom[i])) {
dom.splice(i, 1);
break;
}
}
} else if (dom !== null && to_remove.includes(dom)) {
effect.dom = null;
}
}
import { get } from '../../runtime.js';
import { hydrate_start, hydrating } from '../hydration.js';
import { remove_nodes } from '../operations.js';
import { create_fragment_from_html } from '../reconciler.js';

/**
* @param {Element | Text | Comment} anchor
Expand All @@ -33,20 +13,14 @@ function remove_from_parent_effect(effect, to_remove) {
* @returns {void}
*/
export function html(anchor, get_value, svg, mathml) {
const parent_effect = anchor.parentNode !== current_effect?.dom ? current_effect : null;
let value = derived(get_value);

render_effect(() => {
var dom = html_to_dom(anchor, get(value), svg, mathml);
var [start, end] = html_to_dom(anchor, get(value), svg, mathml);

if (dom) {
return () => {
if (parent_effect !== null) {
remove_from_parent_effect(parent_effect, is_array(dom) ? dom : [dom]);
}
remove(dom);
};
}
return () => {
remove_nodes(start, end);
};
});
}

Expand All @@ -58,10 +32,12 @@ export function html(anchor, get_value, svg, mathml) {
* @param {V} value
* @param {boolean} svg
* @param {boolean} mathml
* @returns {Element | Comment | (Element | Comment | Text)[]}
* @returns {[import('#client').TemplateNode, import('#client').TemplateNode]}
*/
function html_to_dom(target, value, svg, mathml) {
if (hydrating) return hydrate_nodes;
if (hydrating) {
return [hydrate_start, hydrate_start];
}

var html = value + '';
if (svg) html = `<svg>${html}</svg>`;
Expand All @@ -79,10 +55,11 @@ function html_to_dom(target, value, svg, mathml) {
if (node.childNodes.length === 1) {
var child = /** @type {Text | Element | Comment} */ (node.firstChild);
target.before(child);
return child;
return [child, child];
}

var nodes = /** @type {Array<Text | Element | Comment>} */ ([...node.childNodes]);
var first = /** @type {import('#client').TemplateNode} */ (node.firstChild);
var last = /** @type {import('#client').TemplateNode} */ (node.lastChild);

if (svg || mathml) {
while (node.firstChild) {
Expand All @@ -92,5 +69,5 @@ function html_to_dom(target, value, svg, mathml) {
target.before(node);
}

return nodes;
return [first, last];
}
5 changes: 2 additions & 3 deletions packages/svelte/src/internal/client/dom/blocks/if.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { EFFECT_TRANSPARENT } from '../../constants.js';
import { hydrate_nodes, hydrating, set_hydrating } from '../hydration.js';
import { remove } from '../reconciler.js';
import { hydrating, remove_hydrate_nodes, set_hydrating } from '../hydration.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { HYDRATION_END_ELSE } from '../../../../constants.js';

Expand Down Expand Up @@ -42,7 +41,7 @@ export function if_block(
if (condition === is_else) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen with `{#if browser}...{/if}`, for example
remove(hydrate_nodes);
remove_hydrate_nodes();
set_hydrating(false);
mismatch = true;
}
Expand Down
10 changes: 5 additions & 5 deletions packages/svelte/src/internal/client/dom/blocks/svelte-head.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrate_nodes } from '../hydration.js';
import { hydrate_anchor, hydrate_start, hydrating, set_hydrate_nodes } from '../hydration.js';
import { empty } from '../operations.js';
import { block } from '../../reactivity/effects.js';
import { HYDRATION_END, HYDRATION_START } from '../../../../constants.js';
import { HYDRATION_START } from '../../../../constants.js';

/**
* @type {Node | undefined}
Expand All @@ -19,14 +19,14 @@ export function reset_head_anchor() {
export function head(render_fn) {
// The head function may be called after the first hydration pass and ssr comment nodes may still be present,
// therefore we need to skip that when we detect that we're not in hydration mode.
let previous_hydrate_nodes = null;
let previous_hydrate_start = null;
let was_hydrating = hydrating;

/** @type {Comment | Text} */
var anchor;

if (hydrating) {
previous_hydrate_nodes = hydrate_nodes;
previous_hydrate_start = hydrate_start;

// There might be multiple head blocks in our app, so we need to account for each one needing independent hydration.
if (head_anchor === undefined) {
Expand All @@ -50,7 +50,7 @@ export function head(render_fn) {
block(() => render_fn(anchor));
} finally {
if (was_hydrating) {
set_hydrate_nodes(/** @type {import('#client').TemplateNode[]} */ (previous_hydrate_nodes));
set_hydrate_nodes(/** @type {import('#client').TemplateNode} */ (previous_hydrate_start));
}
}
}
67 changes: 39 additions & 28 deletions packages/svelte/src/internal/client/dom/hydration.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DEV } from 'esm-env';
import { HYDRATION_END, HYDRATION_START, HYDRATION_ERROR } from '../../../constants.js';
import * as w from '../warnings.js';
import { remove_nodes } from './operations.js';

/**
* Use this variable to guard everything related to hydration code so it can be treeshaken out
Expand All @@ -13,64 +14,51 @@ export function set_hydrating(value) {
hydrating = value;
}

/**
* Array of nodes to traverse for hydration. This will be null if we're not hydrating, but for
* the sake of simplicity we're not going to use `null` checks everywhere and instead rely on
* the `hydrating` flag to tell whether or not we're in hydration mode at which point this is set.
* @type {import('#client').TemplateNode[]}
*/
export let hydrate_nodes = /** @type {any} */ (null);

/** @type {import('#client').TemplateNode} */
export let hydrate_start;
export let hydrate_start = /** @type {any} */ (null);

/** @param {import('#client').TemplateNode[]} nodes */
export function set_hydrate_nodes(nodes) {
hydrate_nodes = nodes;
hydrate_start = nodes && nodes[0];
/**
* @param {import('#client').TemplateNode} start
*/
export function set_hydrate_nodes(start) {
hydrate_start = start;
}

/**
* This function is only called when `hydrating` is true. If passed a `<!--[-->` opening
* hydration marker, it finds the corresponding closing marker and sets `hydrate_nodes`
* to everything between the markers, before returning the closing marker.
* hydration marker, it sets `hydrate_start` to be the next node and returns the closing marker
* @param {Node} node
* @returns {Node}
*/
export function hydrate_anchor(node) {
if (node.nodeType !== 8) {
return node;
}

var current = /** @type {Node | null} */ (node);

// TODO this could have false positives, if a user comment consisted of `[`. need to tighten that up
if (/** @type {Comment} */ (current).data !== HYDRATION_START) {
if (node.nodeType !== 8 || /** @type {Comment} */ (node).data !== HYDRATION_START) {
return node;
}

/** @type {Node[]} */
var nodes = [];
hydrate_start = /** @type {import('#client').TemplateNode} */ (
/** @type {Comment} */ (node).nextSibling
);

var current = hydrate_start;
var depth = 0;

while ((current = /** @type {Node} */ (current).nextSibling) !== null) {
while (current !== null) {
if (current.nodeType === 8) {
var data = /** @type {Comment} */ (current).data;

if (data === HYDRATION_START) {
depth += 1;
} else if (data[0] === HYDRATION_END) {
if (depth === 0) {
hydrate_nodes = /** @type {import('#client').TemplateNode[]} */ (nodes);
hydrate_start = /** @type {import('#client').TemplateNode} */ (nodes[0]);
return current;
}

depth -= 1;
}
}

nodes.push(current);
current = /** @type {import('#client').TemplateNode} */ (current.nextSibling);
}

let location;
Expand All @@ -86,3 +74,26 @@ export function hydrate_anchor(node) {
w.hydration_mismatch(location);
throw HYDRATION_ERROR;
}

export function remove_hydrate_nodes() {
/** @type {import('#client').TemplateNode | null} */
var node = hydrate_start;
var depth = 0;

while (node) {
if (node.nodeType === 8) {
var data = /** @type {Comment} */ (node).data;

if (data === HYDRATION_START) {
depth += 1;
} else if (data[0] === HYDRATION_END) {
if (depth === 0) return;
depth -= 1;
}
}

var next = /** @type {import('#client').TemplateNode | null} */ (node.nextSibling);
node.remove();
node = next;
}
}
37 changes: 21 additions & 16 deletions packages/svelte/src/internal/client/dom/operations.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { hydrate_anchor, hydrate_start, hydrating } from './hydration.js';
import { DEV } from 'esm-env';
import { init_array_prototype_warnings } from '../dev/equality.js';
import { current_effect } from '../runtime.js';

// export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */
Expand Down Expand Up @@ -69,7 +68,7 @@ export function child(node) {
}

/**
* @param {DocumentFragment | import('#client').TemplateNode[]} fragment
* @param {DocumentFragment} fragment
* @param {boolean} is_text
* @returns {Node | null}
*/
Expand All @@ -83,14 +82,8 @@ export function first_child(fragment, is_text) {
// if an {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
if (is_text && hydrate_start?.nodeType !== 3) {
var text = empty();
var dom = /** @type {import('#client').TemplateNode[]} */ (
/** @type {import('#client').Effect} */ (current_effect).dom
);

dom.unshift(text);
const text = empty();
hydrate_start?.before(text);

return text;
}

Expand All @@ -114,14 +107,8 @@ export function sibling(node, is_text = false) {
// if a sibling {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
if (is_text && next_sibling?.nodeType !== 3) {
var text = empty();
var dom = /** @type {import('#client').TemplateNode[]} */ (
/** @type {import('#client').Effect} */ (current_effect).dom
);

dom.unshift(text);
const text = empty();
next_sibling?.before(text);

return text;
}

Expand All @@ -142,3 +129,21 @@ export function clear_text_content(node) {
export function create_element(name) {
return document.createElement(name);
}

/**
* Remove all nodes between `from` and `to`, inclusive
* @param {import('#client').TemplateNode} from
* @param {import('#client').TemplateNode} to
*/
export function remove_nodes(from, to) {
var node = from;

while (node) {
var next = node.nextSibling;

node.remove();
if (node === to) break;

node = /** @type {import('#client').TemplateNode} */ (next);
}
}
16 changes: 0 additions & 16 deletions packages/svelte/src/internal/client/dom/reconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,3 @@ export function create_fragment_from_html(html) {
elem.innerHTML = html;
return elem.content;
}

/**
* @param {import('#client').Dom} current
*/
export function remove(current) {
if (is_array(current)) {
for (var i = 0; i < current.length; i++) {
var node = current[i];
if (node.isConnected) {
node.remove();
}
}
} else if (current.isConnected) {
current.remove();
}
}
Loading
Loading