chore: simplify hydration (#10943)

* WIP

* unused

* unused

* fix head hydration

* working

* simplify

* tighten up

* css props

* fix treeshaking

* add a comment
pull/10947/head
Rich Harris 9 months ago committed by GitHub
parent afe589e219
commit 4fcedb2fb1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -109,7 +109,7 @@ const bundle = await bundle_code(
).js.code
);
if (!bundle.includes('hydrate_nodes')) {
if (!bundle.includes('hydrate_nodes') && !bundle.includes('hydrate_anchor')) {
// eslint-disable-next-line no-console
console.error(`✅ Hydration code treeshakeable`);
} else {

@ -1045,7 +1045,7 @@ function create_block(parent, name, nodes, context) {
);
/** @type {import('estree').Expression[]} */
const args = [b.id('$$anchor'), template_name];
const args = [template_name];
if (state.metadata.context.template_needs_import_node) {
args.push(b.false);
@ -1089,7 +1089,7 @@ function create_block(parent, name, nodes, context) {
if (use_comment_template) {
// special case — we can use `$.comment` instead of creating a unique template
body.push(b.var(id, b.call('$.comment', b.id('$$anchor'))));
body.push(b.var(id, b.call('$.comment')));
} else {
state.hoisted.push(
b.var(
@ -1103,7 +1103,7 @@ function create_block(parent, name, nodes, context) {
);
/** @type {import('estree').Expression[]} */
const args = [b.id('$$anchor'), template_name];
const args = [template_name];
if (state.metadata.context.template_needs_import_node) {
args.push(b.false);

@ -1,5 +1,4 @@
import { is_promise } from '../../../common.js';
import { hydrate_block_anchor } from '../hydration.js';
import { remove } from '../reconciler.js';
import {
current_component_context,
@ -23,8 +22,6 @@ import { INERT } from '../../constants.js';
export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
const component_context = current_component_context;
hydrate_block_anchor(anchor);
/** @type {any} */
let input;

@ -1,5 +1,5 @@
import { namespace_svg } from '../../../../constants.js';
import { hydrate_nodes, hydrate_block_anchor, hydrating } from '../hydration.js';
import { hydrate_anchor, hydrate_nodes, hydrating } from '../hydration.js';
import { empty } from '../operations.js';
import { render_effect } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js';
@ -12,8 +12,6 @@ import { remove } from '../reconciler.js';
* @returns {void}
*/
export function css_props(anchor, is_html, props, component) {
hydrate_block_anchor(anchor);
/** @type {HTMLElement | SVGElement} */
let element;
@ -24,7 +22,9 @@ export function css_props(anchor, is_html, props, component) {
// Hydration: css props element is surrounded by a ssr comment ...
element = /** @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} */ (element.firstChild);
component_anchor = /** @type {Comment} */ (
hydrate_anchor(/** @type {Comment} */ (element.firstChild))
);
} else {
if (is_html) {
element = document.createElement('div');

@ -6,13 +6,7 @@ import {
EACH_ITEM_REACTIVE,
EACH_KEYED
} from '../../../../constants.js';
import {
hydrate_nodes,
hydrate_block_anchor,
hydrating,
set_hydrating,
update_hydrate_nodes
} from '../hydration.js';
import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrating } from '../hydration.js';
import { empty } from '../operations.js';
import { insert, remove } from '../reconciler.js';
import { untrack } from '../../runtime.js';
@ -59,11 +53,15 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
var state = { flags, items: [] };
var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
hydrate_block_anchor(is_controlled ? /** @type {Node} */ (anchor.firstChild) : anchor);
if (is_controlled) {
var parent_node = /** @type {Element} */ (anchor);
parent_node.append((anchor = empty()));
anchor = hydrating
? /** @type {Comment | Text} */ (
hydrate_anchor(/** @type {Comment | Text} */ (parent_node.firstChild))
)
: parent_node.appendChild(empty());
}
/** @type {import('#client').Effect | null} */
@ -115,14 +113,11 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
if (hydrating) {
var b_items = [];
// Hydrate block
var hydration_list = /** @type {import('#client').TemplateNode[]} */ (hydrate_nodes);
var hydrating_node = hydration_list[0];
/** @type {Node} */
var child_anchor = hydrate_nodes[0];
for (var i = 0; i < length; i++) {
var nodes = update_hydrate_nodes(hydrating_node);
if (nodes === null) {
if (child_anchor.nodeType !== 8 || /** @type {Comment} */ (child_anchor).data !== '[') {
// 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;
@ -130,17 +125,19 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
break;
}
child_anchor = hydrate_anchor(child_anchor);
b_items[i] = create_item(array[i], keys?.[i], i, render_fn, flags);
// TODO helperise this
hydrating_node = /** @type {import('#client').TemplateNode} */ (
/** @type {Node} */ (
/** @type {Node} */ (nodes[nodes.length - 1] || hydrating_node).nextSibling
).nextSibling
);
child_anchor = /** @type {Comment} */ (child_anchor.nextSibling);
}
remove_excess_hydration_nodes(hydration_list, hydrating_node);
// remove excess nodes
if (length > 0) {
while (child_anchor !== anchor) {
var next = /** @type {import('#client').TemplateNode} */ (child_anchor.nextSibling);
/** @type {import('#client').TemplateNode} */ (child_anchor).remove();
child_anchor = next;
}
}
state.items = b_items;
}
@ -440,20 +437,6 @@ function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) {
});
}
/**
* The server could have rendered more list items than the client specifies.
* In that case, we need to remove the remaining server-rendered nodes.
* @param {import('#client').TemplateNode[]} hydration_list
* @param {import('#client').TemplateNode | null} next_node
*/
function remove_excess_hydration_nodes(hydration_list, next_node) {
if (next_node === null) return;
var idx = hydration_list.indexOf(next_node);
if (idx !== -1 && hydration_list.length > idx + 1) {
remove(hydration_list.slice(idx));
}
}
/**
* Longest Increased Subsequence algorithm
* @param {Int32Array} a

@ -1,5 +1,5 @@
import { IS_ELSEIF } from '../../constants.js';
import { hydrate_nodes, hydrate_block_anchor, hydrating, set_hydrating } from '../hydration.js';
import { hydrate_nodes, hydrating, set_hydrating } from '../hydration.js';
import { remove } from '../reconciler.js';
import {
destroy_effect,
@ -23,8 +23,6 @@ export function if_block(
alternate_fn = null,
elseif = false
) {
hydrate_block_anchor(anchor);
/** @type {import('#client').Effect | null} */
let consequent_effect = null;

@ -1,5 +1,4 @@
import { UNINITIALIZED } from '../../constants.js';
import { hydrate_block_anchor } from '../hydration.js';
import { remove } from '../reconciler.js';
import { pause_effect, render_effect } from '../../reactivity/effects.js';
import { safe_not_equal } from '../../reactivity/equality.js';
@ -12,8 +11,6 @@ import { safe_not_equal } from '../../reactivity/equality.js';
* @returns {void}
*/
export function key_block(anchor, get_key, render_fn) {
hydrate_block_anchor(anchor);
/** @type {V | typeof UNINITIALIZED} */
let key = UNINITIALIZED;

@ -1,4 +1,3 @@
import { hydrate_block_anchor } from '../hydration.js';
import { pause_effect, render_effect } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js';
import { current_effect } from '../../runtime.js';
@ -14,8 +13,6 @@ import { current_effect } from '../../runtime.js';
* @returns {void}
*/
export function component(anchor, get_component, render_fn) {
hydrate_block_anchor(anchor);
/** @type {C} */
let component;

@ -1,5 +1,5 @@
import { namespace_svg } from '../../../../constants.js';
import { hydrate_nodes, hydrate_block_anchor, hydrating } from '../hydration.js';
import { hydrate_anchor, hydrate_nodes, hydrating } from '../hydration.js';
import { empty } from '../operations.js';
import {
destroy_effect,
@ -44,8 +44,6 @@ function swap_block_dom(effect, from, to) {
export function element(anchor, get_tag, is_svg, render_fn) {
const parent_effect = /** @type {import('#client').Effect} */ (current_effect);
hydrate_block_anchor(anchor);
/** @type {string | null} */
let tag;
@ -114,7 +112,7 @@ export function element(anchor, get_tag, is_svg, render_fn) {
// 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.firstChild && hydrate_anchor(/** @type {Comment} */ (element.firstChild))
: element.appendChild(empty());
if (child_anchor) {

@ -1,4 +1,4 @@
import { hydrate_nodes, hydrating, set_hydrate_nodes, update_hydrate_nodes } from '../hydration.js';
import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrate_nodes } from '../hydration.js';
import { empty } from '../operations.js';
import { render_effect } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js';
@ -15,7 +15,13 @@ export function head(render_fn) {
if (hydrating) {
previous_hydrate_nodes = hydrate_nodes;
update_hydrate_nodes(document.head.firstChild);
let anchor = /** @type {import('#client').TemplateNode} */ (document.head.firstChild);
while (anchor.nodeType !== 8 || /** @type {Comment} */ (anchor).data !== '[') {
anchor = /** @type {import('#client').TemplateNode} */ (anchor.nextSibling);
}
anchor = /** @type {import('#client').TemplateNode} */ (hydrate_anchor(anchor));
}
var anchor = document.head.appendChild(empty());

@ -98,7 +98,7 @@ if (typeof HTMLElement === 'function') {
* @param {Element} anchor
*/
return (anchor) => {
const node = open(anchor, () => {
const node = open(() => {
const slot = document.createElement('slot');
if (name !== 'default') {
slot.name = name;

@ -1,6 +1,3 @@
import { schedule_task } from './task.js';
import { empty } from './operations.js';
/**
* Use this variable to guard everything related to hydration code so it can be treeshaken out
* if the user doesn't use the `hydrate` method and these code paths are therefore not needed.
@ -29,102 +26,46 @@ export function set_hydrate_nodes(nodes) {
}
/**
* @param {Node | null} first
* @param {boolean} [insert_text] Whether to insert an empty text node if `nodes` is empty
* 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.
* @param {Node} node
* @returns {Node}
*/
export function update_hydrate_nodes(first, insert_text) {
const nodes = get_hydrate_nodes(first, insert_text);
set_hydrate_nodes(nodes);
return nodes;
}
export function hydrate_anchor(node) {
if (node.nodeType !== 8) {
return node;
}
/**
* Returns all nodes between the first `<![>...<!]>` comment tag pair encountered.
* @param {Node | null} node
* @param {boolean} [insert_text] Whether to insert an empty text node if `nodes` is empty
* @returns {import('#client').TemplateNode[] | null}
*/
function get_hydrate_nodes(node, insert_text = false) {
/** @type {import('#client').TemplateNode[]} */
var nodes = [];
var current = /** @type {Node | null} */ (node);
var current_node = /** @type {null | import('#client').TemplateNode} */ (node);
// TODO this could have false positives, if a user comment consisted of `[`. need to tighten that up
if (/** @type {Comment} */ (current)?.data !== '[') {
return node;
}
/** @type {Node[]} */
var nodes = [];
var depth = 0;
var will_start = false;
var started = false;
while (current_node !== null) {
if (current_node.nodeType === 8) {
var data = /** @type {Comment} */ (current_node).data;
while ((current = /** @type {Node} */ (current).nextSibling) !== null) {
if (current.nodeType === 8) {
var data = /** @type {Comment} */ (current).data;
if (data === '[') {
depth += 1;
will_start = true;
} else if (data === ']') {
if (!started) {
// TODO get rid of this — it exists because each blocks are doubly wrapped
return null;
}
if (--depth === 0) {
if (insert_text && nodes.length === 0) {
var text = empty();
nodes.push(text);
current_node.before(text);
if (depth === 0) {
hydrate_nodes = /** @type {import('#client').TemplateNode[]} */ (nodes);
return current;
}
return nodes;
}
depth -= 1;
}
}
if (started) {
nodes.push(current_node);
nodes.push(current);
}
current_node = /** @type {null | import('#client').TemplateNode} */ (current_node.nextSibling);
started = will_start;
}
return null;
}
/**
* @param {Node} node
* @returns {void}
*/
export function hydrate_block_anchor(node) {
if (!hydrating) return;
// @ts-ignore
var nodes = node.$$fragment ?? get_hydrate_nodes(node);
set_hydrate_nodes(nodes);
}
/**
* 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 === '[' &&
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;
schedule_task(() => {
// @ts-expect-error clean up memory
target.$$fragment = undefined;
});
return target;
}
return node;
throw new Error('Expected a closing hydration marker');
}

@ -1,4 +1,4 @@
import { capture_fragment_from_node, hydrate_nodes, hydrating } from './hydration.js';
import { hydrate_anchor, hydrate_nodes, hydrating } from './hydration.js';
import { get_descriptor } from '../utils.js';
// We cache the Node and Element prototype methods, so that we can avoid doing
@ -132,7 +132,7 @@ export function child(node) {
return node.appendChild(empty());
}
return capture_fragment_from_node(child);
return hydrate_anchor(child);
}
/**
@ -159,11 +159,7 @@ export function first_child(fragment, is_text) {
return text;
}
if (first_node !== null) {
return capture_fragment_from_node(first_node);
}
return first_node;
return hydrate_anchor(first_node);
}
/**
@ -176,7 +172,10 @@ export function first_child(fragment, is_text) {
export function sibling(node, is_text = false) {
const next_sibling = next_sibling_get.call(node);
if (hydrating) {
if (!hydrating) {
return next_sibling;
}
// 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) {
@ -192,12 +191,7 @@ export function sibling(node, is_text = false) {
return text;
}
if (next_sibling !== null) {
return capture_fragment_from_node(next_sibling);
}
}
return next_sibling;
return hydrate_anchor(/** @type {Node} */ (next_sibling));
}
/**

@ -1,5 +1,4 @@
import { append_child } from './operations.js';
import { hydrate_nodes, hydrate_block_anchor, hydrating } from './hydration.js';
import { hydrate_nodes, hydrating } from './hydration.js';
import { is_array } from '../utils.js';
/** @param {string} html */
@ -74,7 +73,6 @@ export function remove(current) {
* @returns {Element | Comment | (Element | Comment | Text)[]}
*/
export function reconcile_html(target, value, svg) {
hydrate_block_anchor(target);
if (hydrating) {
return hydrate_nodes;
}

@ -22,18 +22,6 @@ function process_raf_task() {
run_all(tasks);
}
/**
* @param {() => void} fn
* @returns {void}
*/
export function schedule_task(fn) {
if (!is_task_queued) {
is_task_queued = true;
setTimeout(process_task, 0);
}
current_queued_tasks.push(fn);
}
/**
* @param {() => void} fn
* @returns {void}

@ -1,4 +1,4 @@
import { hydrate_nodes, hydrate_block_anchor, hydrating } from './hydration.js';
import { hydrate_nodes, hydrating } from './hydration.js';
import { child, clone_node, empty } from './operations.js';
import {
create_fragment_from_html,
@ -83,18 +83,12 @@ export function svg_template_with_script(svg, return_fragment) {
/**
* @param {boolean} is_fragment
* @param {boolean} use_clone_node
* @param {null | Text | Comment | Element} anchor
* @param {() => Node} [template_element_fn]
* @returns {Element | DocumentFragment | Node[]}
*/
/*#__NO_SIDE_EFFECTS__*/
function open_template(is_fragment, use_clone_node, anchor, template_element_fn) {
function open_template(is_fragment, use_clone_node, template_element_fn) {
if (hydrating) {
if (anchor !== null) {
// TODO why is this sometimes null and sometimes not? needs clear documentation
hydrate_block_anchor(anchor);
}
return is_fragment ? hydrate_nodes : /** @type {Element} */ (hydrate_nodes[0]);
}
@ -104,23 +98,21 @@ function open_template(is_fragment, use_clone_node, anchor, template_element_fn)
}
/**
* @param {null | Text | Comment | Element} anchor
* @param {() => Node} template_element_fn
* @param {boolean} [use_clone_node]
* @returns {Element}
*/
export function open(anchor, template_element_fn, use_clone_node = true) {
return /** @type {Element} */ (open_template(false, use_clone_node, anchor, template_element_fn));
export function open(template_element_fn, use_clone_node = true) {
return /** @type {Element} */ (open_template(false, use_clone_node, template_element_fn));
}
/**
* @param {null | Text | Comment | Element} anchor
* @param {() => Node} template_element_fn
* @param {boolean} [use_clone_node]
* @returns {Element | DocumentFragment | Node[]}
*/
export function open_frag(anchor, template_element_fn, use_clone_node = true) {
return open_template(true, use_clone_node, anchor, template_element_fn);
export function open_frag(template_element_fn, use_clone_node = true) {
return open_template(true, use_clone_node, template_element_fn);
}
const space_template = template(' ', false);
@ -132,7 +124,7 @@ const comment_template = template('<!>', true);
/*#__NO_SIDE_EFFECTS__*/
export function space_frag(anchor) {
/** @type {Node | null} */
var node = /** @type {any} */ (open(anchor, space_template));
var node = /** @type {any} */ (open(space_template));
// if an {expression} is empty during SSR, there might be no
// text node to hydrate (or an anchor comment is falsely detected instead)
// — we must therefore create one
@ -161,12 +153,9 @@ export function space(anchor) {
return anchor;
}
/**
* @param {null | Text | Comment | Element} anchor
*/
/*#__NO_SIDE_EFFECTS__*/
export function comment(anchor) {
return open_frag(anchor, comment_template);
export function comment() {
return open_frag(comment_template);
}
/**

@ -11,12 +11,11 @@ import { remove } from './dom/reconciler.js';
import { flush_sync, push, pop, current_component_context } from './runtime.js';
import { render_effect, destroy_effect } from './reactivity/effects.js';
import {
hydrate_anchor,
hydrate_nodes,
hydrate_block_anchor,
hydrating,
set_hydrate_nodes,
set_hydrating,
update_hydrate_nodes
set_hydrating
} from './dom/hydration.js';
import { array_from } from './utils.js';
import { handle_event_propagation } from './dom/elements/events.js';
@ -66,7 +65,6 @@ export function set_text(dom, value) {
* @param {null | ((anchor: Comment) => void)} fallback_fn
*/
export function slot(anchor, slot_fn, slot_props, fallback_fn) {
hydrate_block_anchor(anchor);
if (slot_fn === undefined) {
if (fallback_fn !== null) {
fallback_fn(anchor);
@ -138,27 +136,25 @@ export function hydrate(component, options) {
const first_child = /** @type {ChildNode} */ (container.firstChild);
const previous_hydrate_nodes = hydrate_nodes;
// Call with insert_text == true to prevent empty {expressions} resulting in an empty
// `nodes` array, resulting in a hydration error down the line
// TODO is both this and the `container.appendChild(anchor)` below necessary?
const nodes = update_hydrate_nodes(first_child, true);
set_hydrating(true);
let hydrated = false;
try {
// Don't flush previous effects to ensure order of outer effects stays consistent
return flush_sync(() => {
const anchor = nodes === null ? container.appendChild(empty()) : null;
set_hydrating(true);
const anchor = hydrate_anchor(first_child);
const instance = _mount(component, { ...options, anchor });
// flush_sync will run this callback and then synchronously run any pending effects,
// which don't belong to the hydration phase anymore - therefore reset it here
set_hydrating(false);
hydrated = true;
return instance;
}, false);
} catch (error) {
if (!hydrated && options.recover !== false && nodes !== null) {
if (!hydrated && options.recover !== false) {
// eslint-disable-next-line no-console
console.error(
'ERR_SVELTE_HYDRATION_MISMATCH' +
@ -188,7 +184,7 @@ export function hydrate(component, options) {
* @param {import('../../main/public.js').ComponentType<import('../../main/public.js').SvelteComponent<Props, Events>>} Component
* @param {{
* target: Document | Element | ShadowRoot;
* anchor: null | Text;
* anchor: null | Node;
* props?: Props;
* events?: { [Property in keyof Events]: (e: Events[Property]) => any };
* context?: Map<any, any>;

@ -25,7 +25,8 @@ export default async function (target) {
target,
props: config.props,
intro: config.intro,
hydrate: __HYDRATE__
hydrate: __HYDRATE__,
recover: false
},
config.options || {}
);

@ -261,7 +261,7 @@ async function run_test_variant(
target,
immutable: config.immutable,
intro: config.intro,
recover: config.recover === undefined ? false : config.recover,
recover: config.recover ?? false,
hydrate: variant === 'hydrate'
});

@ -7,7 +7,7 @@ export default function Bind_this($$anchor, $$props) {
$.push($$props, false);
$.init();
var fragment = $.comment($$anchor);
var fragment = $.comment();
var node = $.first_child(fragment);
$.bind_this(Foo(node, {}), ($$value) => foo = $$value, () => foo);

@ -11,7 +11,7 @@ export default function Main($$anchor, $$props) {
// needs to be a snapshot test because jsdom does auto-correct the attribute casing
let x = 'test';
let y = () => 'test';
var fragment = $.open_frag($$anchor, frag, false);
var fragment = $.open_frag(frag, false);
var div = $.first_child(fragment);
var svg = $.sibling($.sibling(div, true));
var custom_element = $.sibling($.sibling(svg, true));

@ -7,7 +7,7 @@ export default function Each_string_template($$anchor, $$props) {
$.push($$props, false);
$.init();
var fragment = $.comment($$anchor);
var fragment = $.comment();
var node = $.first_child(fragment);
$.each_indexed(node, 1, () => ['foo', 'bar', 'baz'], ($$anchor, thing, $$index) => {

@ -13,7 +13,7 @@ export default function Function_prop_no_getter($$anchor, $$props) {
}
const plusOne = (num) => num + 1;
var fragment = $.comment($$anchor);
var fragment = $.comment();
var node = $.first_child(fragment);
Button(node, {

@ -9,7 +9,7 @@ export default function Hello_world($$anchor, $$props) {
$.push($$props, false);
$.init();
var h1 = $.open($$anchor, frag);
var h1 = $.open(frag);
$.close($$anchor, h1);
$.pop();

@ -17,7 +17,7 @@ export default function State_proxy_literal($$anchor, $$props) {
let str = $.source('');
let tpl = $.source(``);
var fragment = $.open_frag($$anchor, frag);
var fragment = $.open_frag(frag);
var input = $.first_child(fragment);
$.remove_input_attr_defaults(input);

@ -7,7 +7,7 @@ export default function Svelte_element($$anchor, $$props) {
$.push($$props, true);
let tag = $.prop($$props, "tag", 3, 'hr');
var fragment = $.comment($$anchor);
var fragment = $.comment();
var node = $.first_child(fragment);
$.element(node, tag, false);

Loading…
Cancel
Save