diff --git a/.changeset/spotty-shrimps-hug.md b/.changeset/spotty-shrimps-hug.md new file mode 100644 index 000000000000..7ddfdf518bc4 --- /dev/null +++ b/.changeset/spotty-shrimps-hug.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: single-pass hydration diff --git a/packages/svelte/scripts/check-treeshakeability.js b/packages/svelte/scripts/check-treeshakeability.js index 7b9fb816beb3..90d5ab022c7e 100644 --- a/packages/svelte/scripts/check-treeshakeability.js +++ b/packages/svelte/scripts/check-treeshakeability.js @@ -113,7 +113,7 @@ const bundle = await bundle_code( ).js.code ); -if (!bundle.includes('hydrate_nodes') && !bundle.includes('hydrate_anchor')) { +if (!bundle.includes('hydrate_node') && !bundle.includes('hydrate_next')) { // eslint-disable-next-line no-console console.error(`✅ Hydration code treeshakeable`); } else { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index deaf7dd20853..0de47791afb0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -980,7 +980,8 @@ function serialize_inline_component(node, component_name, context, anchor = cont statements.push( b.stmt(b.call('$.css_props', anchor, b.thunk(b.object(custom_css_props)))), - b.stmt(fn(b.member(anchor, b.id('lastChild')))) + b.stmt(fn(b.member(anchor, b.id('lastChild')))), + b.stmt(b.call('$.reset', anchor)) ); } else { context.state.template.push(''); @@ -1441,6 +1442,12 @@ function process_children(nodes, expression, is_element, { visit, state }) { } if (sequence.length > 0) { + // if the final item in a fragment is static text, + // we need to force `hydrate_node` to advance + if (sequence.length === 1 && sequence[0].type === 'Text' && nodes.length > 1) { + state.init.push(b.stmt(b.call('$.next'))); + } + flush_sequence(sequence); } } @@ -1569,7 +1576,7 @@ export const template_visitors = { const namespace = infer_namespace(context.state.metadata.namespace, parent, node.nodes); - const { hoisted, trimmed, is_standalone } = clean_nodes( + const { hoisted, trimmed, is_standalone, is_text_first } = clean_nodes( parent, node.nodes, context.path, @@ -1619,6 +1626,11 @@ export const template_visitors = { context.visit(node, state); } + if (is_text_first) { + // skip over inserted comment + body.push(b.stmt(b.call('$.next'))); + } + /** * @param {import('estree').Identifier} template_name * @param {import('estree').Expression[]} args @@ -1677,11 +1689,7 @@ export const template_visitors = { state }); - body.push( - b.var(id, b.call('$.text', b.id('$$anchor'))), - ...state.before_init, - ...state.init - ); + body.push(b.var(id, b.call('$.text')), ...state.before_init, ...state.init); close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); } else { if (is_standalone) { @@ -1689,8 +1697,7 @@ export const template_visitors = { process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state }); } else { /** @type {(is_text: boolean) => import('estree').Expression} */ - const expression = (is_text) => - is_text ? b.call('$.first_child', id, b.true) : b.call('$.first_child', id); + const expression = (is_text) => b.call('$.first_child', id, is_text && b.true); process_children(trimmed, expression, false, { ...context, state }); @@ -2180,18 +2187,30 @@ export const template_visitors = { context.visit(node, child_state); } - process_children( - trimmed, - () => - b.call( - '$.child', - node.name === 'template' - ? b.member(context.state.node, b.id('content')) - : context.state.node - ), - true, - { ...context, state: child_state } - ); + /** @type {import('estree').Expression} */ + let arg = context.state.node; + + // If `hydrate_node` is set inside the element, we need to reset it + // after the element has been hydrated + let needs_reset = trimmed.some((node) => node.type !== 'Text'); + + // The same applies if it's a `