diff --git a/.changeset/dry-pillows-exist.md b/.changeset/dry-pillows-exist.md new file mode 100644 index 0000000000..dab22ccef1 --- /dev/null +++ b/.changeset/dry-pillows-exist.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: more efficient hydration markers diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index ac71edfd01..19c626ae58 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -24,8 +24,14 @@ import { create_attribute, is_custom_element_node, is_element_node } from '../.. import { error } from '../../../errors.js'; import { binding_properties } from '../../bindings.js'; import { regex_starts_with_newline, regex_whitespaces_strict } from '../../patterns.js'; -import { DOMBooleanAttributes, HYDRATION_END, HYDRATION_START } from '../../../../constants.js'; +import { + DOMBooleanAttributes, + HYDRATION_END, + HYDRATION_END_ELSE, + HYDRATION_START +} from '../../../../constants.js'; import { sanitize_template_string } from '../../../utils/sanitize_template_string.js'; +import { BLOCK_CLOSE, BLOCK_CLOSE_ELSE } from '../../../../internal/server/hydration.js'; export const block_open = t_string(``); export const block_close = t_string(``); @@ -1499,55 +1505,46 @@ const template_visitors = { b.update('++', index, false), b.block(each) ); + + const close = b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE))); + if (node.fallback) { - const fallback_stmts = create_block(node, node.fallback.nodes, context); - fallback_stmts.unshift( - b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(''))) - ); + const fallback = create_block(node, node.fallback.nodes, context); + + fallback.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE_ELSE)))); + state.template.push( t_statement( b.if( b.binary('!==', b.member(array_id, b.id('length')), b.literal(0)), - for_loop, - b.block(fallback_stmts) + b.block([for_loop, close]), + b.block(fallback) ) ) ); } else { - state.template.push(t_statement(for_loop)); + state.template.push(t_statement(for_loop), t_statement(close)); } - state.template.push(block_close); }, IfBlock(node, context) { const state = context.state; state.template.push(block_open); - // Insert ssr:if:true/false anchors in addition to the other anchors so that - // the if block can catch hydration mismatches (false on the server, true on the client and vice versa) - // and continue hydration without having to re-render everything from scratch. - const consequent = create_block(node, node.consequent.nodes, context); - consequent.unshift( - b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(''))) - ); + const alternate = node.alternate ? create_block(node, node.alternate.nodes, context) : []; - const alternate = node.alternate - ? /** @type {import('estree').BlockStatement} */ (context.visit(node.alternate)) - : b.block([]); - alternate.body.unshift( - b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(''))) - ); + consequent.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE)))); + alternate.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE_ELSE)))); state.template.push( t_statement( b.if( /** @type {import('estree').Expression} */ (context.visit(node.test)), - b.block(/** @type {import('estree').Statement[]} */ (consequent)), - alternate + b.block(consequent), + b.block(alternate) ) ) ); - state.template.push(block_close); }, AwaitBlock(node, context) { const state = context.state; diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index 5c9408d314..159cbfb7b6 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -21,6 +21,7 @@ export const TEMPLATE_USE_IMPORT_NODE = 1 << 1; export const HYDRATION_START = '['; export const HYDRATION_END = ']'; +export const HYDRATION_END_ELSE = `${HYDRATION_END}!`; // used to indicate that an `{:else}...` block was rendered export const UNINITIALIZED = Symbol(); diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index ec380eee2d..0ebbf442f7 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -5,6 +5,7 @@ import { EACH_IS_STRICT_EQUALS, EACH_ITEM_REACTIVE, EACH_KEYED, + HYDRATION_END_ELSE, HYDRATION_START } from '../../../../constants.js'; import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrating } from '../hydration.js'; @@ -97,16 +98,13 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re let mismatch = false; if (hydrating) { - var is_else = /** @type {Comment} */ (hydrate_nodes?.[0])?.data === 'ssr:each_else'; + var is_else = /** @type {Comment} */ (anchor).data === HYDRATION_END_ELSE; if (is_else !== (length === 0)) { // hydration mismatch — remove the server-rendered DOM and start over 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[]} */ (hydrate_nodes).shift(); } } diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 27accd8d61..277abacfbd 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -2,6 +2,7 @@ import { IS_ELSEIF } from '../../constants.js'; import { hydrate_nodes, hydrating, set_hydrating } from '../hydration.js'; import { remove } from '../reconciler.js'; import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; +import { HYDRATION_END_ELSE } from '../../../../constants.js'; /** * @param {Comment} anchor @@ -34,21 +35,14 @@ export function if_block( let mismatch = false; if (hydrating) { - const comment_text = /** @type {Comment} */ (hydrate_nodes?.[0])?.data; + const is_else = anchor.data === HYDRATION_END_ELSE; - if ( - !comment_text || - (comment_text === 'ssr:if:true' && !condition) || - (comment_text === 'ssr:if:false' && condition) - ) { + if (condition === is_else) { // Hydration mismatch: remove everything inside the anchor and start fresh. - // This could happen using when `{#if browser} .. {/if}` in SvelteKit. + // This could happen with `{#if browser}...{/if}`, for example remove(hydrate_nodes); set_hydrating(false); mismatch = true; - } else { - // Remove the ssr:if comment node or else it will confuse the subsequent hydration algorithm - hydrate_nodes.shift(); } } diff --git a/packages/svelte/src/internal/client/dom/hydration.js b/packages/svelte/src/internal/client/dom/hydration.js index ef157f8344..5d3c98f2f0 100644 --- a/packages/svelte/src/internal/client/dom/hydration.js +++ b/packages/svelte/src/internal/client/dom/hydration.js @@ -53,7 +53,7 @@ export function hydrate_anchor(node) { if (data === HYDRATION_START) { depth += 1; - } else if (data === HYDRATION_END) { + } else if (data[0] === HYDRATION_END) { if (depth === 0) { hydrate_nodes = /** @type {import('#client').TemplateNode[]} */ (nodes); return current; diff --git a/packages/svelte/src/internal/server/hydration.js b/packages/svelte/src/internal/server/hydration.js index 941918456b..20913cd54c 100644 --- a/packages/svelte/src/internal/server/hydration.js +++ b/packages/svelte/src/internal/server/hydration.js @@ -1,4 +1,5 @@ -import { HYDRATION_END, HYDRATION_START } from '../../constants.js'; +import { HYDRATION_END, HYDRATION_END_ELSE, HYDRATION_START } from '../../constants.js'; export const BLOCK_OPEN = ``; export const BLOCK_CLOSE = ``; +export const BLOCK_CLOSE_ELSE = ``; diff --git a/packages/svelte/tests/hydration/samples/surrounding-whitespace/_override.html b/packages/svelte/tests/hydration/samples/surrounding-whitespace/_override.html index a4b8d9e17c..e728b682d0 100644 --- a/packages/svelte/tests/hydration/samples/surrounding-whitespace/_override.html +++ b/packages/svelte/tests/hydration/samples/surrounding-whitespace/_override.html @@ -1,2 +1,2 @@ - hello + hello diff --git a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js index baabeb63e0..afb1daa750 100644 --- a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js @@ -17,6 +17,6 @@ export default function Each_string_template($$payload, $$props) { $$payload.out += ""; } - $$payload.out += ``; + $$payload.out += ""; $.pop(); } \ No newline at end of file