Skip to content

chore: single-pass hydration #11770

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

Closed
wants to merge 48 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 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
d6c3738
avoid wrappers for <svelte:element>
Rich-Harris May 24, 2024
e705a67
half working css props
Rich-Harris May 24, 2024
5e2cda3
simplify
Rich-Harris May 24, 2024
c8c8848
update snapshots
Rich-Harris May 24, 2024
3296520
fix
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
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { filename, locator } from '../../../state.js';

export const block_open = t_string(`<!--${HYDRATION_START}-->`);
export const block_close = t_string(`<!--${HYDRATION_END}-->`);
export const block_anchor = t_string(`<!---->`);

/**
* @param {string} value
Expand Down Expand Up @@ -959,9 +960,12 @@ function serialize_element_spread_attributes(
* @param {import('#compiler').Component | import('#compiler').SvelteComponent | import('#compiler').SvelteSelf} node
* @param {string | import('estree').Expression} component_name
* @param {import('./types').ComponentContext} context
* @returns {import('estree').Statement}
* @returns {import('estree').Statement[]}
*/
function serialize_inline_component(node, component_name, context) {
/** @type {import('./types').Template[]} */
const parts = [];

/** @type {Array<import('estree').Property[] | import('estree').Expression>} */
const props_and_spreads = [];

Expand Down Expand Up @@ -1123,37 +1127,43 @@ function serialize_inline_component(node, component_name, context) {
b.array(props_and_spreads.map((p) => (Array.isArray(p) ? b.object(p) : p)))
);

/** @type {import('estree').Statement} */
let statement = b.stmt(
(typeof component_name === 'string' ? b.call : b.maybe_call)(
context.state.options.dev
? b.call(
'$.validate_component',
typeof component_name === 'string' ? b.id(component_name) : component_name
)
: component_name,
b.id('$$payload'),
props_expression
)
);
/** @type {import('estree').Statement[]} */
let statements = [
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(block_open.value))),
b.stmt(
(typeof component_name === 'string' ? b.call : b.maybe_call)(
context.state.options.dev
? b.call(
'$.validate_component',
typeof component_name === 'string' ? b.id(component_name) : component_name
)
: component_name,
b.id('$$payload'),
props_expression
)
),
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(block_close.value)))
];

if (custom_css_props.length > 0) {
statement = b.stmt(
b.call(
'$.css_props',
b.id('$$payload'),
b.literal(context.state.metadata.namespace === 'svg' ? false : true),
b.object(custom_css_props),
b.thunk(b.block([statement]))
statements = [
b.stmt(
b.call(
'$.css_props',
b.id('$$payload'),
b.literal(context.state.metadata.namespace === 'svg' ? false : true),
b.object(custom_css_props),
b.thunk(b.block(statements))
)
)
);
];
}

if (snippet_declarations.length > 0) {
statement = b.block([...snippet_declarations, statement]);
statements = [b.block([...snippet_declarations, ...statements])];
}

return statement;
return statements;
}

/**
Expand Down Expand Up @@ -1470,8 +1480,6 @@ const template_visitors = {
}
};

context.state.template.push(block_open);

const main = create_block(node, node.fragment.nodes, {
...context,
state: { ...context.state, metadata }
Expand Down Expand Up @@ -1506,7 +1514,7 @@ const template_visitors = {
)
)
),
block_close
block_anchor
);
if (context.state.options.dev) {
context.state.template.push(t_statement(b.stmt(b.call('$.pop_element'))));
Expand Down Expand Up @@ -1654,29 +1662,27 @@ const template_visitors = {
}
},
Component(node, context) {
const state = context.state;
state.template.push(block_open);
const call = serialize_inline_component(node, node.name, context);
state.template.push(t_statement(call));
state.template.push(block_close);
context.state.template.push(
...serialize_inline_component(node, node.name, context).map((statement) =>
t_statement(statement)
)
);
},
SvelteSelf(node, context) {
const state = context.state;
state.template.push(block_open);
const call = serialize_inline_component(node, context.state.analysis.name, context);
state.template.push(t_statement(call));
state.template.push(block_close);
context.state.template.push(
...serialize_inline_component(node, context.state.analysis.name, context).map((statement) =>
t_statement(statement)
)
);
},
SvelteComponent(node, context) {
const state = context.state;
state.template.push(block_open);
const call = serialize_inline_component(
node,
/** @type {import('estree').Expression} */ (context.visit(node.expression)),
context
context.state.template.push(
...serialize_inline_component(
node,
/** @type {import('estree').Expression} */ (context.visit(node.expression)),
context
).map((statement) => t_statement(statement))
);
state.template.push(t_statement(call));
state.template.push(block_close);
},
LetDirective(node, { state }) {
if (node.expression && node.expression.type !== 'Identifier') {
Expand Down
11 changes: 6 additions & 5 deletions packages/svelte/src/internal/client/dom/blocks/css-props.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { namespace_svg } from '../../../../constants.js';
import { hydrate_anchor, hydrate_start, hydrating } from '../hydration.js';
import { hydrate_anchor, hydrating } from '../hydration.js';
import { empty } from '../operations.js';
import { render_effect } from '../../reactivity/effects.js';

/**
* @param {Element | Text | Comment} anchor
* @param {HTMLElement | SVGElement | Comment} node
* @param {boolean} is_html
* @param {() => Record<string, string>} props
* @param {(anchor: Element | Text | Comment) => any} component
* @returns {void}
*/
export function css_props(anchor, is_html, props, component) {
export function css_props(node, is_html, props, component) {
/** @type {HTMLElement | SVGElement} */
let element;

Expand All @@ -19,7 +19,8 @@ export function css_props(anchor, is_html, props, component) {

if (hydrating) {
// Hydration: css props element is surrounded by a ssr comment ...
element = /** @type {HTMLElement | SVGElement} */ (hydrate_start);
element = /** @type {HTMLElement | SVGElement} */ (node);

// ... and the child(ren) of the css props element is also surround by a ssr comment
component_anchor = /** @type {Comment} */ (
hydrate_anchor(/** @type {Comment} */ (element.firstChild))
Expand All @@ -32,7 +33,7 @@ export function css_props(anchor, is_html, props, component) {
element = document.createElementNS(namespace_svg, 'g');
}

anchor.before(element);
node.before(element); // TODO do we even need an anchor? Can we just pass in the correct element directly from the template?
component_anchor = element.appendChild(empty());
}

Expand Down
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
13 changes: 8 additions & 5 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 { hydrate_anchor, hydrate_start, hydrating } from '../hydration.js';
import { hydrate_anchor, hydrating } from '../hydration.js';
import { empty } from '../operations.js';
import {
block,
Expand All @@ -14,15 +14,15 @@ import { current_component_context } from '../../runtime.js';
import { DEV } from 'esm-env';

/**
* @param {Comment} anchor
* @param {Comment | Element} node
* @param {() => string} get_tag
* @param {boolean} is_svg
* @param {undefined | ((element: Element, anchor: Node | null) => void)} render_fn,
* @param {undefined | (() => string)} get_namespace
* @param {undefined | [number, number]} location
* @returns {void}
*/
export function element(anchor, get_tag, is_svg, render_fn, get_namespace, location) {
export function element(node, get_tag, is_svg, render_fn, get_namespace, location) {
const filename = DEV && location && current_component_context?.function.filename;

/** @type {string | null} */
Expand All @@ -32,7 +32,9 @@ export function element(anchor, get_tag, is_svg, render_fn, get_namespace, locat
let current_tag;

/** @type {null | Element} */
let element = null;
let element = hydrating && node.nodeType === 1 ? /** @type {Element} */ (node) : null;

let anchor = /** @type {Comment} */ (hydrating && element ? element.nextSibling : node);

/** @type {import('#client').Effect | null} */
let effect;
Expand All @@ -51,6 +53,7 @@ export function element(anchor, get_tag, is_svg, render_fn, get_namespace, locat
: is_svg || next_tag === 'svg'
? namespace_svg
: null;

// Assumption: Noone changes the namespace but not the tag (what would that even mean?)
if (next_tag === tag) return;

Expand Down Expand Up @@ -79,7 +82,7 @@ export function element(anchor, get_tag, is_svg, render_fn, get_namespace, locat
if (next_tag && next_tag !== current_tag) {
effect = branch(() => {
element = hydrating
? /** @type {Element} */ (hydrate_start)
? /** @type {Element} */ (element)
: ns
? document.createElementNS(ns, next_tag)
: document.createElement(next_tag);
Expand Down
Loading
Loading