feat: skip static nodes (#12914)

* step one

* WIP

* more

* fix

* collapse sequential sibling calls

* working

* working but messy

* tidy up

* unused

* tweak

* tweak

* tidy

* tweak

* tweak

* revert

* changeset

* Update packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>

* revert this bit

* align

* comments

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
pull/12922/head
Rich Harris 3 months ago committed by GitHub
parent b2214d1c5b
commit 23bce2da20
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: skip over static nodes in compiled client code

@ -1,6 +1,7 @@
/** @import { Expression } from 'estree' */ /** @import { Expression } from 'estree' */
/** @import { ExpressionTag, SvelteNode, Text } from '#compiler' */ /** @import { ExpressionTag, SvelteNode, Text } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../../types' */ /** @import { ComponentClientTransformState, ComponentContext } from '../../types' */
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js'; import * as b from '../../../../../utils/builders.js';
import { build_template_literal, build_update } from './utils.js'; import { build_template_literal, build_update } from './utils.js';
@ -9,42 +10,71 @@ import { build_template_literal, build_update } from './utils.js';
* (e.g. `{a} b {c}`) into a single update function. Along the way it creates * (e.g. `{a} b {c}`) into a single update function. Along the way it creates
* corresponding template node references these updates are applied to. * corresponding template node references these updates are applied to.
* @param {SvelteNode[]} nodes * @param {SvelteNode[]} nodes
* @param {(is_text: boolean) => Expression} expression * @param {(is_text: boolean) => Expression} initial
* @param {boolean} is_element * @param {boolean} is_element
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
export function process_children(nodes, expression, is_element, { visit, state }) { export function process_children(nodes, initial, is_element, { visit, state }) {
const within_bound_contenteditable = state.metadata.bound_contenteditable; const within_bound_contenteditable = state.metadata.bound_contenteditable;
let prev = initial;
let skipped = 0;
/** @typedef {Array<Text | ExpressionTag>} Sequence */ /** @typedef {Array<Text | ExpressionTag>} Sequence */
/** @type {Sequence} */ /** @type {Sequence} */
let sequence = []; let sequence = [];
/** @param {boolean} is_text */
function get_node(is_text) {
if (skipped === 0) {
return prev(is_text);
}
return b.call(
'$.sibling',
prev(false),
(is_text || skipped !== 1) && b.literal(skipped),
is_text && b.true
);
}
/**
* @param {boolean} is_text
* @param {string} name
*/
function flush_node(is_text, name) {
const expression = get_node(is_text);
let id = expression;
if (id.type !== 'Identifier') {
id = b.id(state.scope.generate(name));
state.init.push(b.var(id, expression));
}
prev = () => id;
skipped = 1; // the next node is `$.sibling(id)`
return id;
}
/** /**
* @param {Sequence} sequence * @param {Sequence} sequence
*/ */
function flush_sequence(sequence) { function flush_sequence(sequence) {
if (sequence.length === 1) { if (sequence.length === 1 && sequence[0].type === 'Text') {
const node = sequence[0]; skipped += 1;
state.template.push(sequence[0].raw);
if (node.type === 'Text') { return;
let prev = expression;
expression = () => b.call('$.sibling', prev(false));
state.template.push(node.raw);
return;
}
} }
// if this is a standalone `{expression}`, make sure we handle the case where
// no text node was created because the expression was empty during SSR
const needs_hydration_check = sequence.length === 1;
const id = get_node_id(expression(needs_hydration_check), state, 'text');
state.template.push(' '); state.template.push(' ');
const { has_state, has_call, value } = build_template_literal(sequence, visit, state); const { has_state, has_call, value } = build_template_literal(sequence, visit, state);
// if this is a standalone `{expression}`, make sure we handle the case where
// no text node was created because the expression was empty during SSR
const is_text = sequence.length === 1;
const id = flush_node(is_text, 'text');
const update = b.stmt(b.call('$.set_text', id, value)); const update = b.stmt(b.call('$.set_text', id, value));
if (has_call && !within_bound_contenteditable) { if (has_call && !within_bound_contenteditable) {
@ -54,13 +84,9 @@ export function process_children(nodes, expression, is_element, { visit, state }
} else { } else {
state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value))); state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value)));
} }
expression = (is_text) => b.call('$.sibling', id, is_text && b.true);
} }
for (let i = 0; i < nodes.length; i += 1) { for (const node of nodes) {
const node = nodes[i];
if (node.type === 'Text' || node.type === 'ExpressionTag') { if (node.type === 'Text' || node.type === 'ExpressionTag') {
sequence.push(node); sequence.push(node);
} else { } else {
@ -69,60 +95,62 @@ export function process_children(nodes, expression, is_element, { visit, state }
sequence = []; sequence = [];
} }
if ( let child_state = state;
node.type === 'SvelteHead' ||
node.type === 'TitleElement' || if (is_static_element(node)) {
node.type === 'SnippetBlock' skipped += 1;
) { } else if (node.type === 'EachBlock' && nodes.length === 1 && is_element) {
// These nodes do not contribute to the sibling/child tree node.metadata.is_controlled = true;
// TODO what about e.g. ConstTag and all the other things that
// get hoisted inside clean_nodes?
visit(node, state);
} else { } else {
if (node.type === 'EachBlock' && nodes.length === 1 && is_element) { const id = flush_node(false, node.type === 'RegularElement' ? node.name : 'node');
node.metadata.is_controlled = true; child_state = { ...state, node: id };
visit(node, state);
} else {
const id = get_node_id(
expression(false),
state,
node.type === 'RegularElement' ? node.name : 'node'
);
expression = (is_text) => b.call('$.sibling', id, is_text && b.true);
visit(node, {
...state,
node: id
});
}
} }
visit(node, child_state);
} }
} }
if (sequence.length > 0) { if (sequence.length > 0) {
// if the final item in a fragment is static text,
// we need to force `hydrate_node` to advance
if (sequence.length === 1 && sequence[0].type === 'Text' && nodes.length > 1) {
state.init.push(b.stmt(b.call('$.next')));
}
flush_sequence(sequence); flush_sequence(sequence);
} }
// if there are trailing static text nodes/elements,
// traverse to the last (n - 1) one when hydrating
if (skipped > 1) {
skipped -= 1;
state.init.push(b.stmt(get_node(false)));
}
} }
/** /**
* @param {Expression} expression *
* @param {ComponentClientTransformState} state * @param {SvelteNode} node
* @param {string} name
*/ */
function get_node_id(expression, state, name) { function is_static_element(node) {
let id = expression; if (node.type !== 'RegularElement') return false;
if (node.fragment.metadata.dynamic) return false;
if (id.type !== 'Identifier') { for (const attribute of node.attributes) {
id = b.id(state.scope.generate(name)); if (attribute.type !== 'Attribute') {
return false;
}
if (is_event_attribute(attribute)) {
return false;
}
if (attribute.value !== true && !is_text_attribute(attribute)) {
return false;
}
state.init.push(b.var(id, expression)); if (node.name === 'option' && attribute.name === 'value') {
return false;
}
if (node.name.includes('-')) {
return false; // we're setting all attributes on custom elements through properties
}
} }
return id;
return true;
} }

@ -70,6 +70,7 @@ export function create_text(value = '') {
* @param {N} node * @param {N} node
* @returns {Node | null} * @returns {Node | null}
*/ */
/*@__NO_SIDE_EFFECTS__*/
export function get_first_child(node) { export function get_first_child(node) {
return first_child_getter.call(node); return first_child_getter.call(node);
} }
@ -79,6 +80,7 @@ export function get_first_child(node) {
* @param {N} node * @param {N} node
* @returns {Node | null} * @returns {Node | null}
*/ */
/*@__NO_SIDE_EFFECTS__*/
export function get_next_sibling(node) { export function get_next_sibling(node) {
return next_sibling_getter.call(node); return next_sibling_getter.call(node);
} }
@ -137,17 +139,21 @@ export function first_child(fragment, is_text) {
/** /**
* Don't mark this as side-effect-free, hydration needs to walk all nodes * Don't mark this as side-effect-free, hydration needs to walk all nodes
* @template {Node} N * @param {TemplateNode} node
* @param {N} node * @param {number} count
* @param {boolean} is_text * @param {boolean} is_text
* @returns {Node | null} * @returns {Node | null}
*/ */
export function sibling(node, is_text = false) { export function sibling(node, count = 1, is_text = false) {
if (!hydrating) { let next_sibling = hydrating ? hydrate_node : node;
return /** @type {TemplateNode} */ (get_next_sibling(node));
while (count--) {
next_sibling = /** @type {TemplateNode} */ (get_next_sibling(next_sibling));
} }
var next_sibling = /** @type {TemplateNode} */ (get_next_sibling(hydrate_node)); if (!hydrating) {
return next_sibling;
}
var type = next_sibling.nodeType; var type = next_sibling.nodeType;

@ -9,17 +9,17 @@ export default function Main($$anchor) {
let y = () => 'test'; let y = () => 'test';
var fragment = root(); var fragment = root();
var div = $.first_child(fragment); var div = $.first_child(fragment);
var svg = $.sibling($.sibling(div)); var svg = $.sibling(div, 2);
var custom_element = $.sibling($.sibling(svg)); var custom_element = $.sibling(svg, 2);
var div_1 = $.sibling($.sibling(custom_element)); var div_1 = $.sibling(custom_element, 2);
$.template_effect(() => $.set_attribute(div_1, "foobar", y())); $.template_effect(() => $.set_attribute(div_1, "foobar", y()));
var svg_1 = $.sibling($.sibling(div_1)); var svg_1 = $.sibling(div_1, 2);
$.template_effect(() => $.set_attribute(svg_1, "viewBox", y())); $.template_effect(() => $.set_attribute(svg_1, "viewBox", y()));
var custom_element_1 = $.sibling($.sibling(svg_1)); var custom_element_1 = $.sibling(svg_1, 2);
$.template_effect(() => $.set_custom_element_data(custom_element_1, "fooBar", y())); $.template_effect(() => $.set_custom_element_data(custom_element_1, "fooBar", y()));

@ -13,11 +13,11 @@ export default function Purity($$anchor) {
p.textContent = Math.max(min, Math.min(max, number)); p.textContent = Math.max(min, Math.min(max, number));
var p_1 = $.sibling($.sibling(p)); var p_1 = $.sibling(p, 2);
p_1.textContent = location.href; p_1.textContent = location.href;
var node = $.sibling($.sibling(p_1)); var node = $.sibling(p_1, 2);
Child(node, { prop: encodeURIComponent(value) }); Child(node, { prop: encodeURIComponent(value) });
$.append($$anchor, fragment); $.append($$anchor, fragment);

@ -1,10 +1,20 @@
import "svelte/internal/disclose-version"; import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client"; import * as $ from "svelte/internal/client";
var root = $.template(`<header><nav><a href="/">Home</a> <a href="/away">Away</a></nav></header>`); var root = $.template(`<header><nav><a href="/">Home</a> <a href="/away">Away</a></nav></header> <main><h1> </h1> <div class="static"><p>we don't need to traverse these nodes</p></div> <p>or</p> <p>these</p> <p>ones</p> <!></main>`, 1);
export default function Skip_static_subtree($$anchor) { export default function Skip_static_subtree($$anchor, $$props) {
var header = root(); var fragment = root();
var main = $.sibling($.first_child(fragment), 2);
var h1 = $.child(main);
var text = $.child(h1);
$.append($$anchor, header); $.reset(h1);
var node = $.sibling(h1, 10);
$.html(node, () => $$props.content, false, false);
$.reset(main);
$.template_effect(() => $.set_text(text, $$props.title));
$.append($$anchor, fragment);
} }

@ -1,5 +1,7 @@
import * as $ from "svelte/internal/server"; import * as $ from "svelte/internal/server";
export default function Skip_static_subtree($$payload) { export default function Skip_static_subtree($$payload, $$props) {
$$payload.out += `<header><nav><a href="/">Home</a> <a href="/away">Away</a></nav></header>`; let { title, content } = $$props;
$$payload.out += `<header><nav><a href="/">Home</a> <a href="/away">Away</a></nav></header> <main><h1>${$.escape(title)}</h1> <div class="static"><p>we don't need to traverse these nodes</p></div> <p>or</p> <p>these</p> <p>ones</p> ${$.html(content)}</main>`;
} }

@ -1,6 +1,21 @@
<script>
let { title, content } = $props();
</script>
<header> <header>
<nav> <nav>
<a href="/">Home</a> <a href="/">Home</a>
<a href="/away">Away</a> <a href="/away">Away</a>
</nav> </nav>
</header> </header>
<main>
<h1>{title}</h1>
<div class="static">
<p>we don't need to traverse these nodes</p>
</div>
<p>or</p>
<p>these</p>
<p>ones</p>
{@html content}
</main>

@ -18,11 +18,11 @@ export default function State_proxy_literal($$anchor) {
$.remove_input_defaults(input); $.remove_input_defaults(input);
var input_1 = $.sibling($.sibling(input)); var input_1 = $.sibling(input, 2);
$.remove_input_defaults(input_1); $.remove_input_defaults(input_1);
var button = $.sibling($.sibling(input_1)); var button = $.sibling(input_1, 2);
button.__click = [reset, str, tpl]; button.__click = [reset, str, tpl];
$.bind_value(input, () => $.get(str), ($$value) => $.set(str, $$value)); $.bind_value(input, () => $.get(str), ($$value) => $.set(str, $$value));

Loading…
Cancel
Save