perf: skip the anchor comment for a lone dynamic component

`is_standalone` already lets a component render into its parent block's anchor
rather than creating a comment anchor of its own. It was gated off for dynamic
components, so every namespaced component call site paid for an anchor.

`{#each}` is excluded. Its items are reordered by their node boundary, and a
deferred branch is committed at the item's own anchor, so removing that anchor
reorders incorrectly when a swap is still pending.

`$.component` claims the DOM boundary and steps over the closing hydration
marker when there is no anchor to do it, which is otherwise `$.append`'s job.

Saves two DOM nodes per instance at every qualifying call site.
pull/18734/head
Mathias Picker 1 month ago
parent 6374a2a0db
commit 7e9b3c2e8f

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: render a lone dynamic component into its parent block's anchor

@ -287,7 +287,9 @@ export function clean_nodes(
((first.type === 'RenderTag' && !first.metadata.dynamic) ||
(first.type === 'Component' &&
!state.options.hmr &&
!first.metadata.dynamic &&
// a dynamic component renders into a branch of its own, so the enclosing
// effect owns no nodes and `{#each}` reconciliation cannot move it
(!first.metadata.dynamic || parent.type !== 'EachBlock') &&
!first.attributes.some(
(attribute) => attribute.type === 'Attribute' && attribute.name.startsWith('--')
))),

@ -1,4 +1,4 @@
/** @import { TemplateNode, Dom } from '#client' */
/** @import { Effect, TemplateNode, Dom } from '#client' */
import { EFFECT_TRANSPARENT } from '#client/constants';
import { block } from '../../reactivity/effects.js';
import {
@ -10,6 +10,8 @@ import {
set_hydrating,
skip_nodes
} from '../hydration.js';
import { active_effect } from '../../runtime.js';
import { assign_nodes } from '../template.js';
import { BranchManager } from './branches.js';
import { HYDRATION_START, HYDRATION_START_ELSE } from '../../../../constants.js';
@ -58,4 +60,12 @@ export function component(node, get_component, render_fn) {
branches.ensure(component, component && ((target) => render_fn(target, component)));
}, EFFECT_TRANSPARENT);
// If no anchor comment was created for this block — i.e. it is the sole child of its parent
// and renders straight into the parent's anchor — then nothing else will claim the DOM
// boundary or step over the closing hydration marker. Otherwise `$.append` does both.
if (hydrating && /** @type {Effect} */ (active_effect).nodes === null) {
assign_nodes(/** @type {TemplateNode} */ (hydration_start_node), hydrate_node);
hydrate_next();
}
}

@ -0,0 +1,5 @@
<script>
let { id, gate } = $props();
</script>
<span>a{await gate(id)}</span>

@ -0,0 +1,5 @@
<script>
let { id, gate } = $props();
</script>
<span>b{await gate(id)}</span>

@ -0,0 +1,25 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [flip, swap, release] = target.querySelectorAll('button');
const html = () => [...target.querySelectorAll('span')].map((s) => s.textContent).join('');
assert.equal(html(), 'a0a1a2', 'initial');
// swap: the new branches are deferred, so they land offscreen in a fragment
swap.click();
await tick();
// reorder while the swap is still pending
flip.click();
await tick();
for (let i = 0; i < 3; i++) {
release.click();
await tick();
}
await tick();
assert.equal(html(), 'b2b1b0', 'reorder while swap pending');
}
});

@ -0,0 +1,24 @@
<script>
import A from './A.svelte';
import B from './B.svelte';
let items = $state([{ id: 0 }, { id: 1 }, { id: 2 }]);
let Component = $state(A);
const queue = [];
function gate(id) {
if (Component === A) return id;
const p = Promise.withResolvers();
queue.push(() => p.resolve(id));
return p.promise;
}
</script>
<button onclick={() => items.reverse()}>flip</button>
<button onclick={() => (Component = B)}>swap</button>
<button onclick={() => queue.shift()?.()}>release</button>
<svelte:boundary>
{#each items as item (item.id)}<Component id={item.id} {gate} />{/each}
{#snippet pending()}<i>pending</i>{/snippet}
</svelte:boundary>

@ -0,0 +1,5 @@
<script>
let { item } = $props();
</script>
<span>{item}</span>

@ -0,0 +1,12 @@
import 'svelte/internal/disclose-version';
import * as $ from 'svelte/internal/client';
var root = $.from_html(`<span> </span>`);
export default function A($$anchor, $$props) {
var span = root();
var text = $.only_child(span, true);
$.template_effect(() => $.set_text(text, $$props.item));
$.append($$anchor, span);
}

@ -0,0 +1,65 @@
import 'svelte/internal/disclose-version';
import * as $ from 'svelte/internal/client';
import A from './A.svelte';
var root = $.from_html(`<!><span></span>`, 1);
var root_1 = $.from_html(`<!> <!> <!>`, 1);
export default function Lone_dynamic_component($$anchor) {
let Component = $.proxy(A);
let items = $.proxy([1, 2, 3]);
let show = true;
var fragment = root_1();
var node = $.first_child(fragment);
{
var consequent = ($$anchor) => {
$.component($$anchor, () => Component, ($$anchor, Component_1) => {
Component_1($$anchor, {});
});
};
$.if(node, ($$render) => {
if (show) $$render(consequent);
});
}
var node_1 = $.sibling(node, 2);
{
var consequent_1 = ($$anchor) => {
var fragment_2 = root();
var node_2 = $.first_child(fragment_2);
$.component(node_2, () => Component, ($$anchor, Component_2) => {
Component_2($$anchor, {});
});
$.next();
$.append($$anchor, fragment_2);
};
$.if(node_1, ($$render) => {
if (show) $$render(consequent_1);
});
}
var node_3 = $.sibling(node_1, 2);
$.each(node_3, 17, () => items, $.index, ($$anchor, item) => {
var fragment_3 = $.comment();
var node_4 = $.first_child(fragment_3);
$.component(node_4, () => Component, ($$anchor, Component_3) => {
Component_3($$anchor, {
get item() {
return $.get(item);
}
});
});
$.append($$anchor, fragment_3);
});
$.append($$anchor, fragment);
}

@ -0,0 +1,7 @@
import * as $ from 'svelte/internal/server';
export default function A($$renderer, $$props) {
let { item } = $$props;
$$renderer.push(`<span>${$.escape(item)}</span>`);
}

@ -0,0 +1,61 @@
import * as $ from 'svelte/internal/server';
import A from './A.svelte';
export default function Lone_dynamic_component($$renderer) {
let Component = A;
let items = [1, 2, 3];
let show = true;
if (show) {
$$renderer.push('<!--[0-->');
if (Component) {
$$renderer.push('<!--[-->');
Component($$renderer, {});
$$renderer.push('<!--]-->');
} else {
$$renderer.push('<!--[!-->');
$$renderer.push('<!--]-->');
}
} else {
$$renderer.push('<!--[-1-->');
}
$$renderer.push(`<!--]--> `);
if (show) {
$$renderer.push('<!--[0-->');
if (Component) {
$$renderer.push('<!--[-->');
Component($$renderer, {});
$$renderer.push('<!--]-->');
} else {
$$renderer.push('<!--[!-->');
$$renderer.push('<!--]-->');
}
$$renderer.push(`<span></span>`);
} else {
$$renderer.push('<!--[-1-->');
}
$$renderer.push(`<!--]--> <!--[-->`);
const each_array = $.ensure_array_like(items);
for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) {
let item = each_array[$$index];
if (Component) {
$$renderer.push('<!--[-->');
Component($$renderer, { item });
$$renderer.push('<!--]-->');
} else {
$$renderer.push('<!--[!-->');
$$renderer.push('<!--]-->');
}
}
$$renderer.push(`<!--]-->`);
}

@ -0,0 +1,17 @@
<script>
import A from './A.svelte';
let Component = $state(A);
let items = $state([1, 2, 3]);
let show = $state(true);
</script>
<!-- sole child of the block, so it renders into the block's anchor -->
{#if show}<Component />{/if}
<!-- not the sole child, so it still needs an anchor of its own -->
{#if show}<Component /><span></span>{/if}
<!-- `{#each}` items are reordered by their node boundary, and a deferred branch is
committed at the item's own anchor, so the anchor stays -->
{#each items as item}<Component {item} />{/each}
Loading…
Cancel
Save