From c9a8e22dbf6aef10a9aade3ca76434c53e808434 Mon Sep 17 00:00:00 2001 From: adiguba Date: Sun, 9 Feb 2025 13:28:07 +0100 Subject: [PATCH 1/6] rewrite else/if hydration --- .../3-transform/client/visitors/Fragment.js | 4 +- .../3-transform/client/visitors/IfBlock.js | 30 +++++++------- .../3-transform/server/visitors/IfBlock.js | 29 +++++++++----- .../src/internal/client/dom/blocks/if.js | 39 +++++++++++++------ 4 files changed, 67 insertions(+), 35 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index e9cfd9c50684..389a694741fc 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -48,7 +48,9 @@ export function Fragment(node, context) { const is_single_element = trimmed.length === 1 && trimmed[0].type === 'RegularElement'; const is_single_child_not_needing_template = trimmed.length === 1 && - (trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement'); + (trimmed[0].type === 'SvelteFragment' || + trimmed[0].type === 'TitleElement' || + (trimmed[0].type === 'IfBlock' && trimmed[0].elseif)); const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js index d658f9eaf819..7b644238bbdd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Expression } from 'estree' */ +/** @import { BlockStatement, Expression, Identifier } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '../../../../utils/builders.js'; @@ -18,15 +18,25 @@ export function IfBlock(node, context) { let alternate_id; + const r_index = b.id('$$r_index'); + const h_index = b.id('$$h_index'); + if (node.alternate) { - const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate)); alternate_id = context.state.scope.generate('alternate'); - statements.push(b.var(b.id(alternate_id), b.arrow([b.id('$$anchor')], alternate))); + const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate)); + const nodes = node.alternate.nodes; + + let alternate_args = [b.id('$$anchor')]; + if (nodes.length === 1 && nodes[0].type === 'IfBlock' && nodes[0].elseif) { + alternate_args.push(r_index, h_index); + } + + statements.push(b.var(b.id(alternate_id), b.arrow(alternate_args, alternate))); } /** @type {Expression[]} */ const args = [ - context.state.node, + node.elseif ? b.id('$$anchor') : context.state.node, b.arrow( [b.id('$$render')], b.block([ @@ -34,13 +44,7 @@ export function IfBlock(node, context) { /** @type {Expression} */ (context.visit(node.test)), b.stmt(b.call(b.id('$$render'), b.id(consequent_id))), alternate_id - ? b.stmt( - b.call( - b.id('$$render'), - b.id(alternate_id), - node.alternate ? b.literal(false) : undefined - ) - ) + ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.literal(false))) : undefined ) ]) @@ -69,10 +73,10 @@ export function IfBlock(node, context) { // ...even though they're logically equivalent. In the first case, the // transition will only play when `y` changes, but in the second it // should play when `x` or `y` change — both are considered 'local' - args.push(b.literal(true)); + args.push(r_index, h_index); } statements.push(b.stmt(b.call('$.if', ...args))); - context.state.init.push(b.block(statements)); + context.state.init.push(...statements); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js index 4df09aa8b948..8b64d2ebb12c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Expression } from 'estree' */ +/** @import { BlockStatement, Expression, IfStatement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js'; @@ -10,19 +10,30 @@ import { block_close, block_open } from './shared/utils.js'; * @param {ComponentContext} context */ export function IfBlock(node, context) { - const test = /** @type {Expression} */ (context.visit(node.test)); + let index = 0; const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent)); + consequent.body.unshift(b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open))); + let if_statement = b.if(/** @type {Expression} */ (context.visit(node.test)), consequent); - const alternate = node.alternate - ? /** @type {BlockStatement} */ (context.visit(node.alternate)) - : b.block([]); + context.state.template.push(if_statement, block_close); - consequent.body.unshift(b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open))); + let alt = node.alternate; + while (alt && alt.nodes.length === 1 && alt.nodes[0].type === 'IfBlock' && alt.nodes[0].elseif) { + const elseif = alt.nodes[0]; + const alternate = /** @type {BlockStatement} */ (context.visit(elseif.consequent)); + alternate.body.unshift( + b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(``))) + ); + if_statement = if_statement.alternate = b.if( + /** @type {Expression} */ (context.visit(elseif.test)), + alternate + ); + alt = elseif.alternate; + } - alternate.body.unshift( + if_statement.alternate = alt ? /** @type {BlockStatement} */ (context.visit(alt)) : b.block([]); + if_statement.alternate.body.unshift( b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN_ELSE))) ); - - context.state.template.push(b.if(test, consequent, alternate), block_close); } diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 36790c05c135..e58d0aacfdc1 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -9,16 +9,17 @@ import { set_hydrating } from '../hydration.js'; import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; -import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; +import { HYDRATION_START, HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; /** * @param {TemplateNode} node - * @param {(branch: (fn: (anchor: Node) => void, flag?: boolean) => void) => void} fn - * @param {boolean} [elseif] True if this is an `{:else if ...}` block rather than an `{#if ...}`, as that affects which transitions are considered 'local' + * @param {(branch: (fn: (anchor: Node, r_index?: number, h_index?: number) => void, flag?: boolean) => void) => void} fn + * @param {number} [root_index] + * @param {number} [hydrate_index] * @returns {void} */ -export function if_block(node, fn, elseif = false) { - if (hydrating) { +export function if_block(node, fn, root_index = 0, hydrate_index) { + if (hydrating && root_index === 0) { hydrate_next(); } @@ -33,28 +34,41 @@ export function if_block(node, fn, elseif = false) { /** @type {UNINITIALIZED | boolean | null} */ var condition = UNINITIALIZED; - var flags = elseif ? EFFECT_TRANSPARENT : 0; + var flags = root_index > 0 ? EFFECT_TRANSPARENT : 0; var has_branch = false; - const set_branch = (/** @type {(anchor: Node) => void} */ fn, flag = true) => { + const set_branch = ( + /** @type {(anchor: Node, r_index?: number, h_index?: number) => void} */ fn, + flag = true + ) => { has_branch = true; update_branch(flag, fn); }; const update_branch = ( /** @type {boolean | null} */ new_condition, - /** @type {null | ((anchor: Node) => void)} */ fn + /** @type {null | ((anchor: Node, r_index?: number, h_index?: number) => void)} */ fn ) => { if (condition === (condition = new_condition)) return; /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ let mismatch = false; - if (hydrating) { - const is_else = /** @type {Comment} */ (anchor).data === HYDRATION_START_ELSE; + if (hydrating && hydrate_index != -1) { + if (hydrate_index === undefined) { + const data = /** @type {Comment} */ (anchor).data; + if (data === HYDRATION_START) { + hydrate_index = 0; + } else if (data === HYDRATION_START_ELSE) { + hydrate_index = Infinity; + } else { + hydrate_index = parseInt(data.substring(1)); + } + } + const is_else = hydrate_index > root_index; - if (!!condition === is_else) { + if (!!condition === is_else || isNaN(hydrate_index)) { // Hydration mismatch: remove everything inside the anchor and start fresh. // This could happen with `{#if browser}...{/if}`, for example anchor = remove_nodes(); @@ -62,6 +76,7 @@ export function if_block(node, fn, elseif = false) { set_hydrate_node(anchor); set_hydrating(false); mismatch = true; + hydrate_index = -1; // ignore hydration in next else if } } @@ -81,7 +96,7 @@ export function if_block(node, fn, elseif = false) { if (alternate_effect) { resume_effect(alternate_effect); } else if (fn) { - alternate_effect = branch(() => fn(anchor)); + alternate_effect = branch(() => fn(anchor, root_index + 1, hydrate_index)); } if (consequent_effect) { From b13c41981c3f2b3fce8592f0681814ed23870be2 Mon Sep 17 00:00:00 2001 From: adiguba Date: Sun, 9 Feb 2025 14:20:11 +0100 Subject: [PATCH 2/6] fix: bad index --- .../src/compiler/phases/3-transform/server/visitors/IfBlock.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js index 8b64d2ebb12c..cbdd2cd8cc2a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js @@ -10,14 +10,13 @@ import { block_close, block_open } from './shared/utils.js'; * @param {ComponentContext} context */ export function IfBlock(node, context) { - let index = 0; - const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent)); consequent.body.unshift(b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open))); let if_statement = b.if(/** @type {Expression} */ (context.visit(node.test)), consequent); context.state.template.push(if_statement, block_close); + let index = 1; let alt = node.alternate; while (alt && alt.nodes.length === 1 && alt.nodes[0].type === 'IfBlock' && alt.nodes[0].elseif) { const elseif = alt.nodes[0]; From 4bf1f915d3374721f8a1655b1a151c1c423b1a3c Mon Sep 17 00:00:00 2001 From: adiguba Date: Sun, 9 Feb 2025 14:35:36 +0100 Subject: [PATCH 3/6] reduce args on if_block() --- .../3-transform/client/visitors/IfBlock.js | 7 ++----- .../svelte/src/internal/client/dom/blocks/if.js | 17 ++++++++--------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js index 7b644238bbdd..d4de34a50044 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -18,9 +18,6 @@ export function IfBlock(node, context) { let alternate_id; - const r_index = b.id('$$r_index'); - const h_index = b.id('$$h_index'); - if (node.alternate) { alternate_id = context.state.scope.generate('alternate'); const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate)); @@ -28,7 +25,7 @@ export function IfBlock(node, context) { let alternate_args = [b.id('$$anchor')]; if (nodes.length === 1 && nodes[0].type === 'IfBlock' && nodes[0].elseif) { - alternate_args.push(r_index, h_index); + alternate_args.push(b.id('$$elseif')); } statements.push(b.var(b.id(alternate_id), b.arrow(alternate_args, alternate))); @@ -73,7 +70,7 @@ export function IfBlock(node, context) { // ...even though they're logically equivalent. In the first case, the // transition will only play when `y` changes, but in the second it // should play when `x` or `y` change — both are considered 'local' - args.push(r_index, h_index); + args.push(b.id('$$elseif')); } statements.push(b.stmt(b.call('$.if', ...args))); diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index e58d0aacfdc1..c64b3daa81d6 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -13,12 +13,11 @@ import { HYDRATION_START, HYDRATION_START_ELSE, UNINITIALIZED } from '../../../. /** * @param {TemplateNode} node - * @param {(branch: (fn: (anchor: Node, r_index?: number, h_index?: number) => void, flag?: boolean) => void) => void} fn - * @param {number} [root_index] - * @param {number} [hydrate_index] + * @param {(branch: (fn: (anchor: Node, elseif?: [number,number]) => void, flag?: boolean) => void) => void} fn + * @param {[number,number]} [elseif] * @returns {void} */ -export function if_block(node, fn, root_index = 0, hydrate_index) { +export function if_block(node, fn, [root_index, hydrate_index] = [0, 0]) { if (hydrating && root_index === 0) { hydrate_next(); } @@ -39,7 +38,7 @@ export function if_block(node, fn, root_index = 0, hydrate_index) { var has_branch = false; const set_branch = ( - /** @type {(anchor: Node, r_index?: number, h_index?: number) => void} */ fn, + /** @type {(anchor: Node, elseif?: [number,number]) => void} */ fn, flag = true ) => { has_branch = true; @@ -48,15 +47,15 @@ export function if_block(node, fn, root_index = 0, hydrate_index) { const update_branch = ( /** @type {boolean | null} */ new_condition, - /** @type {null | ((anchor: Node, r_index?: number, h_index?: number) => void)} */ fn + /** @type {null | ((anchor: Node, elseif?: [number,number]) => void)} */ fn ) => { if (condition === (condition = new_condition)) return; /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ let mismatch = false; - if (hydrating && hydrate_index != -1) { - if (hydrate_index === undefined) { + if (hydrating && hydrate_index !== -1) { + if (root_index === 0) { const data = /** @type {Comment} */ (anchor).data; if (data === HYDRATION_START) { hydrate_index = 0; @@ -96,7 +95,7 @@ export function if_block(node, fn, root_index = 0, hydrate_index) { if (alternate_effect) { resume_effect(alternate_effect); } else if (fn) { - alternate_effect = branch(() => fn(anchor, root_index + 1, hydrate_index)); + alternate_effect = branch(() => fn(anchor, [root_index + 1, hydrate_index])); } if (consequent_effect) { From d9fe35b655a0887d020cfd7d0aee52c7828f65f0 Mon Sep 17 00:00:00 2001 From: adiguba Date: Tue, 11 Feb 2025 21:57:36 +0100 Subject: [PATCH 4/6] restore the block --- .../src/compiler/phases/3-transform/client/visitors/IfBlock.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js index d4de34a50044..0876fa30b6a5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -75,5 +75,5 @@ export function IfBlock(node, context) { statements.push(b.stmt(b.call('$.if', ...args))); - context.state.init.push(...statements); + context.state.init.push(b.block(statements)); } From b3564e39881d4a7e44e69d50502bc6a79e8f9b99 Mon Sep 17 00:00:00 2001 From: adiguba Date: Tue, 11 Feb 2025 22:02:24 +0100 Subject: [PATCH 5/6] don't use isNan() --- packages/svelte/src/internal/client/dom/blocks/if.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index c64b3daa81d6..423c436fe4ef 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -63,11 +63,16 @@ export function if_block(node, fn, [root_index, hydrate_index] = [0, 0]) { hydrate_index = Infinity; } else { hydrate_index = parseInt(data.substring(1)); + if (hydrate_index !== hydrate_index) { + // if hydrate_index is NaN + // we set an invalid index to force mismatch + hydrate_index = condition ? Infinity : -1; + } } } const is_else = hydrate_index > root_index; - if (!!condition === is_else || isNaN(hydrate_index)) { + if (!!condition === is_else) { // Hydration mismatch: remove everything inside the anchor and start fresh. // This could happen with `{#if browser}...{/if}`, for example anchor = remove_nodes(); From 157950e7b93549a10690b994314d142af2361051 Mon Sep 17 00:00:00 2001 From: adiguba Date: Thu, 27 Feb 2025 16:08:57 +0100 Subject: [PATCH 6/6] changeset --- .changeset/tough-steaks-travel.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tough-steaks-travel.md diff --git a/.changeset/tough-steaks-travel.md b/.changeset/tough-steaks-travel.md new file mode 100644 index 000000000000..6a9468b609cb --- /dev/null +++ b/.changeset/tough-steaks-travel.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +chore: Reduce hydration comment for {:else if}