feat: more efficient hydration markers (#11019)

* remove <!--ssr:if:true--> comments

* remove <!--ssr:each_else--> comments

* changeset

* tidy up
pull/11023/head
Rich Harris 1 year ago committed by GitHub
parent 4f3fae7c6d
commit 0a162924fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: more efficient hydration markers

@ -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}-->`);
@ -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;

@ -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();

@ -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();
}
}

@ -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();
}
}

@ -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;

@ -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}-->`;

@ -1,2 +1,2 @@
<!-- unrelated comment -->
<!--[--><!--[--><!--ssr:if:true-->hello<!--]--><!--]-->
<!--[--><!--[-->hello<!--]--><!--]-->

@ -17,6 +17,6 @@ export default function Each_string_template($$payload, $$props) {
$$payload.out += "<!--]-->";
}
$$payload.out += `<!--]-->`;
$$payload.out += "<!--]-->";
$.pop();
}
Loading…
Cancel
Save