Skip to content

feat: more efficient hydration markers #11019

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

Merged
merged 4 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/dry-pillows-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

feat: more efficient hydration markers
Original file line number Diff line number Diff line change
Expand Up @@ -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(`<!--${HYDRATION_START}-->`);
export const block_close = t_string(`<!--${HYDRATION_END}-->`);
Expand Down Expand Up @@ -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('<!ssr:each_else>')))
);
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('<!ssr:if:true>')))
);
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('<!ssr:if:false>')))
);
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;
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
6 changes: 2 additions & 4 deletions packages/svelte/src/internal/client/dom/blocks/each.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
}
}

Expand Down
14 changes: 4 additions & 10 deletions packages/svelte/src/internal/client/dom/blocks/if.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/dom/hydration.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion packages/svelte/src/internal/server/hydration.js
Original file line number Diff line number Diff line change
@@ -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 = `<!--${HYDRATION_START}-->`;
export const BLOCK_CLOSE = `<!--${HYDRATION_END}-->`;
export const BLOCK_CLOSE_ELSE = `<!--${HYDRATION_END_ELSE}-->`;
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
<!-- unrelated comment -->
<!--[--><!--[--><!--ssr:if:true-->hello<!--]--><!--]-->
<!--[--><!--[-->hello<!--]--><!--]-->
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ export default function Each_string_template($$payload, $$props) {
$$payload.out += "<!--]-->";
}

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