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 { error } from '../../../errors.js';
import { binding_properties } from '../../bindings.js'; import { binding_properties } from '../../bindings.js';
import { regex_starts_with_newline, regex_whitespaces_strict } from '../../patterns.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 { 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_open = t_string(`<!--${HYDRATION_START}-->`);
export const block_close = t_string(`<!--${HYDRATION_END}-->`); export const block_close = t_string(`<!--${HYDRATION_END}-->`);
@ -1499,55 +1505,46 @@ const template_visitors = {
b.update('++', index, false), b.update('++', index, false),
b.block(each) b.block(each)
); );
const close = b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE)));
if (node.fallback) { if (node.fallback) {
const fallback_stmts = create_block(node, node.fallback.nodes, context); const fallback = create_block(node, node.fallback.nodes, context);
fallback_stmts.unshift(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal('<!ssr:each_else>'))) fallback.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE_ELSE))));
);
state.template.push( state.template.push(
t_statement( t_statement(
b.if( b.if(
b.binary('!==', b.member(array_id, b.id('length')), b.literal(0)), b.binary('!==', b.member(array_id, b.id('length')), b.literal(0)),
for_loop, b.block([for_loop, close]),
b.block(fallback_stmts) b.block(fallback)
) )
) )
); );
} else { } 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) { IfBlock(node, context) {
const state = context.state; const state = context.state;
state.template.push(block_open); 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); const consequent = create_block(node, node.consequent.nodes, context);
consequent.unshift( const alternate = node.alternate ? create_block(node, node.alternate.nodes, context) : [];
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal('<!ssr:if:true>')))
);
const alternate = node.alternate consequent.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE))));
? /** @type {import('estree').BlockStatement} */ (context.visit(node.alternate)) alternate.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE_ELSE))));
: b.block([]);
alternate.body.unshift(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal('<!ssr:if:false>')))
);
state.template.push( state.template.push(
t_statement( t_statement(
b.if( b.if(
/** @type {import('estree').Expression} */ (context.visit(node.test)), /** @type {import('estree').Expression} */ (context.visit(node.test)),
b.block(/** @type {import('estree').Statement[]} */ (consequent)), b.block(consequent),
alternate b.block(alternate)
) )
) )
); );
state.template.push(block_close);
}, },
AwaitBlock(node, context) { AwaitBlock(node, context) {
const state = context.state; const state = context.state;

@ -21,6 +21,7 @@ export const TEMPLATE_USE_IMPORT_NODE = 1 << 1;
export const HYDRATION_START = '['; export const HYDRATION_START = '[';
export const HYDRATION_END = ']'; export const HYDRATION_END = ']';
export const HYDRATION_END_ELSE = `${HYDRATION_END}!`; // used to indicate that an `{:else}...` block was rendered
export const UNINITIALIZED = Symbol(); export const UNINITIALIZED = Symbol();

@ -5,6 +5,7 @@ import {
EACH_IS_STRICT_EQUALS, EACH_IS_STRICT_EQUALS,
EACH_ITEM_REACTIVE, EACH_ITEM_REACTIVE,
EACH_KEYED, EACH_KEYED,
HYDRATION_END_ELSE,
HYDRATION_START HYDRATION_START
} from '../../../../constants.js'; } from '../../../../constants.js';
import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrating } from '../hydration.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; let mismatch = false;
if (hydrating) { 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)) { if (is_else !== (length === 0)) {
// hydration mismatch — remove the server-rendered DOM and start over // hydration mismatch — remove the server-rendered DOM and start over
remove(hydrate_nodes); remove(hydrate_nodes);
set_hydrating(false); set_hydrating(false);
mismatch = true; 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 { hydrate_nodes, hydrating, set_hydrating } from '../hydration.js';
import { remove } from '../reconciler.js'; import { remove } from '../reconciler.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { HYDRATION_END_ELSE } from '../../../../constants.js';
/** /**
* @param {Comment} anchor * @param {Comment} anchor
@ -34,21 +35,14 @@ export function if_block(
let mismatch = false; let mismatch = false;
if (hydrating) { if (hydrating) {
const comment_text = /** @type {Comment} */ (hydrate_nodes?.[0])?.data; const is_else = anchor.data === HYDRATION_END_ELSE;
if ( if (condition === is_else) {
!comment_text ||
(comment_text === 'ssr:if:true' && !condition) ||
(comment_text === 'ssr:if:false' && condition)
) {
// Hydration mismatch: remove everything inside the anchor and start fresh. // 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); remove(hydrate_nodes);
set_hydrating(false); set_hydrating(false);
mismatch = true; 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) { if (data === HYDRATION_START) {
depth += 1; depth += 1;
} else if (data === HYDRATION_END) { } else if (data[0] === HYDRATION_END) {
if (depth === 0) { if (depth === 0) {
hydrate_nodes = /** @type {import('#client').TemplateNode[]} */ (nodes); hydrate_nodes = /** @type {import('#client').TemplateNode[]} */ (nodes);
return current; 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_OPEN = `<!--${HYDRATION_START}-->`;
export const BLOCK_CLOSE = `<!--${HYDRATION_END}-->`; export const BLOCK_CLOSE = `<!--${HYDRATION_END}-->`;
export const BLOCK_CLOSE_ELSE = `<!--${HYDRATION_END_ELSE}-->`;

@ -1,2 +1,2 @@
<!-- unrelated comment --> <!-- 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 += `<!--]-->`; $$payload.out += "<!--]-->";
$.pop(); $.pop();
} }
Loading…
Cancel
Save