Skip to content

chore: tidy up hydration code #10891

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

Merged
merged 16 commits into from
Mar 23, 2024
Merged
4 changes: 2 additions & 2 deletions packages/svelte/src/internal/client/dom/blocks/css-props.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { namespace_svg } from '../../../../constants.js';
import { current_hydration_fragment, hydrate_block_anchor, hydrating } from '../hydration.js';
import { hydrate_nodes, hydrate_block_anchor, hydrating } from '../hydration.js';
import { empty } from '../operations.js';
import { render_effect } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js';
Expand All @@ -22,7 +22,7 @@ export function css_props(anchor, is_html, props, component) {

if (hydrating) {
// Hydration: css props element is surrounded by a ssr comment ...
tag = /** @type {HTMLElement | SVGElement} */ (current_hydration_fragment[0]);
tag = /** @type {HTMLElement | SVGElement} */ (hydrate_nodes[0]);
// ... and the child(ren) of the css props element is also surround by a ssr comment
component_anchor = /** @type {Comment} */ (tag.firstChild);
} else {
Expand Down
36 changes: 17 additions & 19 deletions packages/svelte/src/internal/client/dom/blocks/each.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import {
EACH_KEYED
} from '../../../../constants.js';
import {
current_hydration_fragment,
get_hydration_fragment,
hydrate_nodes,
hydrate_block_anchor,
hydrating,
set_current_hydration_fragment
set_hydrating,
update_hydrate_nodes
} from '../hydration.js';
import { empty } from '../operations.js';
import { insert, remove } from '../reconciler.js';
Expand Down Expand Up @@ -98,17 +98,16 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re
let mismatch = false;

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

if (is_else !== (length === 0)) {
// hydration mismatch — remove the server-rendered DOM and start over
remove(current_hydration_fragment);
set_current_hydration_fragment(null);
remove(hydrate_nodes);
set_hydrating(false);
mismatch = true;
} else if (is_else) {
// Remove the each_else comment node or else it will confuse the subsequent hydration algorithm
/** @type {import('#client').TemplateNode[]} */ (current_hydration_fragment).shift();
/** @type {import('#client').TemplateNode[]} */ (hydrate_nodes).shift();
}
}

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

// Hydrate block
var hydration_list = /** @type {import('#client').TemplateNode[]} */ (
current_hydration_fragment
);
var hydration_list = /** @type {import('#client').TemplateNode[]} */ (hydrate_nodes);
var hydrating_node = hydration_list[0];

for (var i = 0; i < length; i++) {
var fragment = get_hydration_fragment(hydrating_node);
set_current_hydration_fragment(fragment);
if (!fragment) {
// If fragment is null, then that means that the server rendered less items than what
// the client code specifies -> break out and continue with client-side node creation
var nodes = update_hydrate_nodes(hydrating_node);

if (nodes === null) {
// If `nodes` is null, then that means that the server rendered fewer items than what
// expected, so break out and continue appending non-hydrated items
mismatch = true;
set_hydrating(false);
break;
}

Expand All @@ -137,7 +135,7 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re
// TODO helperise this
hydrating_node = /** @type {import('#client').TemplateNode} */ (
/** @type {Node} */ (
/** @type {Node} */ (fragment[fragment.length - 1] || hydrating_node).nextSibling
/** @type {Node} */ (nodes[nodes.length - 1] || hydrating_node).nextSibling
).nextSibling
);
}
Expand Down Expand Up @@ -175,8 +173,8 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re
}

if (mismatch) {
// Set a fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]);
// continue in hydration mode
set_hydrating(true);
}
});

Expand Down
19 changes: 7 additions & 12 deletions packages/svelte/src/internal/client/dom/blocks/if.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import { IS_ELSEIF } from '../../constants.js';
import {
current_hydration_fragment,
hydrate_block_anchor,
hydrating,
set_current_hydration_fragment
} from '../hydration.js';
import { hydrate_nodes, hydrate_block_anchor, hydrating, set_hydrating } from '../hydration.js';
import { remove } from '../reconciler.js';
import {
destroy_effect,
Expand Down Expand Up @@ -40,7 +35,7 @@ export function if_block(anchor, get_condition, consequent_fn, alternate_fn, els
let mismatch = false;

if (hydrating) {
const comment_text = /** @type {Comment} */ (current_hydration_fragment?.[0])?.data;
const comment_text = /** @type {Comment} */ (hydrate_nodes?.[0])?.data;

if (
!comment_text ||
Expand All @@ -49,12 +44,12 @@ export function if_block(anchor, get_condition, consequent_fn, alternate_fn, els
) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen using when `{#if browser} .. {/if}` in SvelteKit.
remove(current_hydration_fragment);
set_current_hydration_fragment(null);
remove(hydrate_nodes);
set_hydrating(false);
mismatch = true;
} else {
// Remove the ssr:if comment node or else it will confuse the subsequent hydration algorithm
current_hydration_fragment.shift();
hydrate_nodes.shift();
}
}

Expand Down Expand Up @@ -85,8 +80,8 @@ export function if_block(anchor, get_condition, consequent_fn, alternate_fn, els
}

if (mismatch) {
// Set fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]);
// continue in hydration mode
set_hydrating(true);
}
});

Expand Down
25 changes: 14 additions & 11 deletions packages/svelte/src/internal/client/dom/blocks/svelte-element.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { namespace_svg } from '../../../../constants.js';
import { current_hydration_fragment, hydrate_block_anchor, hydrating } from '../hydration.js';
import { hydrate_nodes, hydrate_block_anchor, hydrating } from '../hydration.js';
import { empty } from '../operations.js';
import {
destroy_effect,
Expand Down Expand Up @@ -105,22 +105,25 @@ export function element(anchor, get_tag, is_svg, render_fn) {
effect = render_effect(() => {
const prev_element = element;
element = hydrating
? /** @type {Element} */ (current_hydration_fragment[0])
? /** @type {Element} */ (hydrate_nodes[0])
: ns
? document.createElementNS(ns, next_tag)
: document.createElement(next_tag);

if (render_fn) {
let anchor;
if (hydrating) {
// Use the existing ssr comment as the anchor so that the inner open and close
// methods can pick up the existing nodes correctly
anchor = /** @type {Comment} */ (element.firstChild);
} else {
anchor = empty();
element.appendChild(anchor);
// If hydrating, use the existing ssr comment as the anchor so that the
// inner open and close methods can pick up the existing nodes correctly
var child_anchor = hydrating
? /** @type {Comment} */ (element.firstChild)
: element.appendChild(empty());

if (child_anchor) {
// `child_anchor` can be undefined if this is a void element with children,
// i.e. `<svelte:element this={"hr"}>...</svelte:element>`. This is
// user error, but we warn on it elsewhere (in dev) so here we just
// silently ignore it
render_fn(element, child_anchor);
}
render_fn(element, anchor);
}

anchor.before(element);
Expand Down
23 changes: 8 additions & 15 deletions packages/svelte/src/internal/client/dom/blocks/svelte-head.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
current_hydration_fragment,
get_hydration_fragment,
hydrating,
set_current_hydration_fragment
} from '../hydration.js';
import { hydrate_nodes, hydrating, set_hydrate_nodes, update_hydrate_nodes } from '../hydration.js';
import { empty } from '../operations.js';
import { render_effect } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js';
Expand All @@ -15,14 +10,12 @@ import { remove } from '../reconciler.js';
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 hydration_fragment = null;
let previous_hydration_fragment = null;
let previous_hydrate_nodes = null;
let was_hydrating = hydrating;

let is_hydrating = hydrating;
if (is_hydrating) {
hydration_fragment = get_hydration_fragment(document.head.firstChild);
previous_hydration_fragment = current_hydration_fragment;
set_current_hydration_fragment(hydration_fragment);
if (hydrating) {
previous_hydrate_nodes = hydrate_nodes;
update_hydrate_nodes(document.head.firstChild);
}

try {
Expand Down Expand Up @@ -50,8 +43,8 @@ export function head(render_fn) {
}
};
} finally {
if (is_hydrating) {
set_current_hydration_fragment(previous_hydration_fragment);
if (was_hydrating) {
set_hydrate_nodes(previous_hydrate_nodes);
}
}
}
108 changes: 70 additions & 38 deletions packages/svelte/src/internal/client/dom/hydration.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// Handle hydration

import { schedule_task } from './task.js';
import { empty } from './operations.js';

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

/** @param {boolean} value */
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('../types.js').TemplateNode[]}
* @type {import('#client').TemplateNode[]}
*/
export let current_hydration_fragment = /** @type {any} */ (null);
export let hydrate_nodes = /** @type {any} */ (null);

/**
* @param {null | import('../types.js').TemplateNode[]} fragment
* @param {null | import('#client').TemplateNode[]} nodes
* @returns {void}
*/
export function set_current_hydration_fragment(fragment) {
hydrating = fragment !== null;
current_hydration_fragment = /** @type {import('../types.js').TemplateNode[]} */ (fragment);
export function set_hydrate_nodes(nodes) {
hydrate_nodes = /** @type {import('#client').TemplateNode[]} */ (nodes);
}

/**
* @param {Node | null} first
* @param {boolean} [insert_text] Whether to insert an empty text node if `nodes` is empty
*/
export function update_hydrate_nodes(first, insert_text) {
const nodes = get_hydrate_nodes(first, insert_text);
set_hydrate_nodes(nodes);
return nodes;
}

/**
* Returns all nodes between the first `<!--ssr:...-->` comment tag pair encountered.
* @param {Node | null} node
* @param {boolean} [insert_text] Whether to insert an empty text node if the fragment is empty
* @returns {import('../types.js').TemplateNode[] | null}
* @param {boolean} [insert_text] Whether to insert an empty text node if `nodes` is empty
* @returns {import('#client').TemplateNode[] | null}
*/
export function get_hydration_fragment(node, insert_text = false) {
/** @type {import('../types.js').TemplateNode[]} */
const fragment = [];
function get_hydrate_nodes(node, insert_text = false) {
/** @type {import('#client').TemplateNode[]} */
var nodes = [];

/** @type {null | Node} */
let current_node = node;
var current_node = /** @type {null | import('#client').TemplateNode} */ (node);

/** @type {null | string} */
let target_depth = null;
var target_depth = null;

while (current_node !== null) {
const node_type = current_node.nodeType;
const next_sibling = current_node.nextSibling;
if (node_type === 8) {
const data = /** @type {Comment} */ (current_node).data;
if (current_node.nodeType === 8) {
var data = /** @type {Comment} */ (current_node).data;

if (data.startsWith('ssr:')) {
const depth = data.slice(4);
var depth = data.slice(4);

if (target_depth === null) {
target_depth = depth;
} else if (depth === target_depth) {
if (insert_text && fragment.length === 0) {
const text = empty();
fragment.push(text);
/** @type {Node} */ (current_node.parentNode).insertBefore(text, current_node);
if (insert_text && nodes.length === 0) {
var text = empty();
nodes.push(text);
current_node.before(text);
}
return fragment;
return nodes;
} else {
fragment.push(/** @type {Text | Comment | Element} */ (current_node));
nodes.push(current_node);
}
current_node = next_sibling;
continue;
}
} else if (target_depth !== null) {
nodes.push(current_node);
}
if (target_depth !== null) {
fragment.push(/** @type {Text | Comment | Element} */ (current_node));
}
current_node = next_sibling;

current_node = /** @type {null | import('#client').TemplateNode} */ (current_node.nextSibling);
}

return null;
}

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

if (node.nodeType === 8) {
// @ts-ignore
let fragment = node.$$fragment;
if (fragment === undefined) {
fragment = get_hydration_fragment(node);
let nodes = node.$$fragment;
if (nodes === undefined) {
nodes = get_hydrate_nodes(node);
} else {
schedule_task(() => {
// @ts-expect-error clean up memory
node.$$fragment = undefined;
});
}
set_current_hydration_fragment(fragment);
set_hydrate_nodes(nodes);
} else {
const first_child = /** @type {Element | null} */ (node.firstChild);
set_current_hydration_fragment(first_child === null ? [] : [first_child]);
set_hydrate_nodes(first_child === null ? [] : [first_child]);
}
}

/**
* Expects to only be called in hydration mode
* @param {Node} node
* @returns {Node}
*/
export function capture_fragment_from_node(node) {
if (
node.nodeType === 8 &&
/** @type {Comment} */ (node).data.startsWith('ssr:') &&
hydrate_nodes[hydrate_nodes.length - 1] !== node
) {
const nodes = /** @type {Node[]} */ (get_hydrate_nodes(node));
const last_child = nodes[nodes.length - 1] || node;
const target = /** @type {Node} */ (last_child.nextSibling);
// @ts-ignore
target.$$fragment = nodes;
return target;
}
return node;
}
Loading