Skip to content

Commit 0a16292

Browse files
authored
feat: more efficient hydration markers (#11019)
* remove <!--ssr:if:true--> comments * remove <!--ssr:each_else--> comments * changeset * tidy up
1 parent 4f3fae7 commit 0a16292

File tree

9 files changed

+39
-43
lines changed

9 files changed

+39
-43
lines changed

.changeset/dry-pillows-exist.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
feat: more efficient hydration markers

packages/svelte/src/compiler/phases/3-transform/server/transform-server.js

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,14 @@ import { create_attribute, is_custom_element_node, is_element_node } from '../..
2424
import { error } from '../../../errors.js';
2525
import { binding_properties } from '../../bindings.js';
2626
import { regex_starts_with_newline, regex_whitespaces_strict } from '../../patterns.js';
27-
import { DOMBooleanAttributes, HYDRATION_END, HYDRATION_START } from '../../../../constants.js';
27+
import {
28+
DOMBooleanAttributes,
29+
HYDRATION_END,
30+
HYDRATION_END_ELSE,
31+
HYDRATION_START
32+
} from '../../../../constants.js';
2833
import { sanitize_template_string } from '../../../utils/sanitize_template_string.js';
34+
import { BLOCK_CLOSE, BLOCK_CLOSE_ELSE } from '../../../../internal/server/hydration.js';
2935

3036
export const block_open = t_string(`<!--${HYDRATION_START}-->`);
3137
export const block_close = t_string(`<!--${HYDRATION_END}-->`);
@@ -1499,55 +1505,46 @@ const template_visitors = {
14991505
b.update('++', index, false),
15001506
b.block(each)
15011507
);
1508+
1509+
const close = b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE)));
1510+
15021511
if (node.fallback) {
1503-
const fallback_stmts = create_block(node, node.fallback.nodes, context);
1504-
fallback_stmts.unshift(
1505-
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal('<!ssr:each_else>')))
1506-
);
1512+
const fallback = create_block(node, node.fallback.nodes, context);
1513+
1514+
fallback.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE_ELSE))));
1515+
15071516
state.template.push(
15081517
t_statement(
15091518
b.if(
15101519
b.binary('!==', b.member(array_id, b.id('length')), b.literal(0)),
1511-
for_loop,
1512-
b.block(fallback_stmts)
1520+
b.block([for_loop, close]),
1521+
b.block(fallback)
15131522
)
15141523
)
15151524
);
15161525
} else {
1517-
state.template.push(t_statement(for_loop));
1526+
state.template.push(t_statement(for_loop), t_statement(close));
15181527
}
1519-
state.template.push(block_close);
15201528
},
15211529
IfBlock(node, context) {
15221530
const state = context.state;
15231531
state.template.push(block_open);
15241532

1525-
// Insert ssr:if:true/false anchors in addition to the other anchors so that
1526-
// the if block can catch hydration mismatches (false on the server, true on the client and vice versa)
1527-
// and continue hydration without having to re-render everything from scratch.
1528-
15291533
const consequent = create_block(node, node.consequent.nodes, context);
1530-
consequent.unshift(
1531-
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal('<!ssr:if:true>')))
1532-
);
1534+
const alternate = node.alternate ? create_block(node, node.alternate.nodes, context) : [];
15331535

1534-
const alternate = node.alternate
1535-
? /** @type {import('estree').BlockStatement} */ (context.visit(node.alternate))
1536-
: b.block([]);
1537-
alternate.body.unshift(
1538-
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal('<!ssr:if:false>')))
1539-
);
1536+
consequent.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE))));
1537+
alternate.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE_ELSE))));
15401538

15411539
state.template.push(
15421540
t_statement(
15431541
b.if(
15441542
/** @type {import('estree').Expression} */ (context.visit(node.test)),
1545-
b.block(/** @type {import('estree').Statement[]} */ (consequent)),
1546-
alternate
1543+
b.block(consequent),
1544+
b.block(alternate)
15471545
)
15481546
)
15491547
);
1550-
state.template.push(block_close);
15511548
},
15521549
AwaitBlock(node, context) {
15531550
const state = context.state;

packages/svelte/src/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const TEMPLATE_USE_IMPORT_NODE = 1 << 1;
2121

2222
export const HYDRATION_START = '[';
2323
export const HYDRATION_END = ']';
24+
export const HYDRATION_END_ELSE = `${HYDRATION_END}!`; // used to indicate that an `{:else}...` block was rendered
2425

2526
export const UNINITIALIZED = Symbol();
2627

packages/svelte/src/internal/client/dom/blocks/each.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
EACH_IS_STRICT_EQUALS,
66
EACH_ITEM_REACTIVE,
77
EACH_KEYED,
8+
HYDRATION_END_ELSE,
89
HYDRATION_START
910
} from '../../../../constants.js';
1011
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
9798
let mismatch = false;
9899

99100
if (hydrating) {
100-
var is_else = /** @type {Comment} */ (hydrate_nodes?.[0])?.data === 'ssr:each_else';
101+
var is_else = /** @type {Comment} */ (anchor).data === HYDRATION_END_ELSE;
101102

102103
if (is_else !== (length === 0)) {
103104
// hydration mismatch — remove the server-rendered DOM and start over
104105
remove(hydrate_nodes);
105106
set_hydrating(false);
106107
mismatch = true;
107-
} else if (is_else) {
108-
// Remove the each_else comment node or else it will confuse the subsequent hydration algorithm
109-
/** @type {import('#client').TemplateNode[]} */ (hydrate_nodes).shift();
110108
}
111109
}
112110

packages/svelte/src/internal/client/dom/blocks/if.js

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { IS_ELSEIF } from '../../constants.js';
22
import { hydrate_nodes, hydrating, set_hydrating } from '../hydration.js';
33
import { remove } from '../reconciler.js';
44
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
5+
import { HYDRATION_END_ELSE } from '../../../../constants.js';
56

67
/**
78
* @param {Comment} anchor
@@ -34,21 +35,14 @@ export function if_block(
3435
let mismatch = false;
3536

3637
if (hydrating) {
37-
const comment_text = /** @type {Comment} */ (hydrate_nodes?.[0])?.data;
38+
const is_else = anchor.data === HYDRATION_END_ELSE;
3839

39-
if (
40-
!comment_text ||
41-
(comment_text === 'ssr:if:true' && !condition) ||
42-
(comment_text === 'ssr:if:false' && condition)
43-
) {
40+
if (condition === is_else) {
4441
// Hydration mismatch: remove everything inside the anchor and start fresh.
45-
// This could happen using when `{#if browser} .. {/if}` in SvelteKit.
42+
// This could happen with `{#if browser}...{/if}`, for example
4643
remove(hydrate_nodes);
4744
set_hydrating(false);
4845
mismatch = true;
49-
} else {
50-
// Remove the ssr:if comment node or else it will confuse the subsequent hydration algorithm
51-
hydrate_nodes.shift();
5246
}
5347
}
5448

packages/svelte/src/internal/client/dom/hydration.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export function hydrate_anchor(node) {
5353

5454
if (data === HYDRATION_START) {
5555
depth += 1;
56-
} else if (data === HYDRATION_END) {
56+
} else if (data[0] === HYDRATION_END) {
5757
if (depth === 0) {
5858
hydrate_nodes = /** @type {import('#client').TemplateNode[]} */ (nodes);
5959
return current;
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { HYDRATION_END, HYDRATION_START } from '../../constants.js';
1+
import { HYDRATION_END, HYDRATION_END_ELSE, HYDRATION_START } from '../../constants.js';
22

33
export const BLOCK_OPEN = `<!--${HYDRATION_START}-->`;
44
export const BLOCK_CLOSE = `<!--${HYDRATION_END}-->`;
5+
export const BLOCK_CLOSE_ELSE = `<!--${HYDRATION_END_ELSE}-->`;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
<!-- unrelated comment -->
2-
<!--[--><!--[--><!--ssr:if:true-->hello<!--]--><!--]-->
2+
<!--[--><!--[-->hello<!--]--><!--]-->

packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ export default function Each_string_template($$payload, $$props) {
1717
$$payload.out += "<!--]-->";
1818
}
1919

20-
$$payload.out += `<!--]-->`;
20+
$$payload.out += "<!--]-->";
2121
$.pop();
2222
}

0 commit comments

Comments
 (0)