diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js
index 8c59e3aebab3..baf86a2c9516 100644
--- a/packages/svelte/src/internal/client/dom/blocks/each.js
+++ b/packages/svelte/src/internal/client/dom/blocks/each.js
@@ -10,13 +10,12 @@ import {
} from '../../../../constants.js';
import {
hydrate_anchor,
- hydrate_nodes,
hydrate_start,
hydrating,
+ remove_hydrate_nodes,
set_hydrating
} from '../hydration.js';
import { clear_text_content, empty } from '../operations.js';
-import { remove } from '../reconciler.js';
import { untrack } from '../../runtime.js';
import {
block,
@@ -145,7 +144,7 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
if (is_else !== (length === 0)) {
// hydration mismatch — remove the server-rendered DOM and start over
- remove(hydrate_nodes);
+ remove_hydrate_nodes();
set_hydrating(false);
mismatch = true;
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js
index 103bf7c046db..98fb91c41bfd 100644
--- a/packages/svelte/src/internal/client/dom/blocks/html.js
+++ b/packages/svelte/src/internal/client/dom/blocks/html.js
@@ -1,29 +1,9 @@
import { derived } from '../../reactivity/deriveds.js';
import { render_effect } from '../../reactivity/effects.js';
-import { current_effect, get } from '../../runtime.js';
-import { is_array } from '../../utils.js';
-import { hydrate_nodes, hydrating } from '../hydration.js';
-import { create_fragment_from_html, remove } from '../reconciler.js';
-
-/**
- * @param {import('#client').Effect} effect
- * @param {(Element | Comment | Text)[]} to_remove
- * @returns {void}
- */
-function remove_from_parent_effect(effect, to_remove) {
- const dom = effect.dom;
-
- if (is_array(dom)) {
- for (let i = dom.length - 1; i >= 0; i--) {
- if (to_remove.includes(dom[i])) {
- dom.splice(i, 1);
- break;
- }
- }
- } else if (dom !== null && to_remove.includes(dom)) {
- effect.dom = null;
- }
-}
+import { get } from '../../runtime.js';
+import { hydrate_start, hydrating } from '../hydration.js';
+import { remove_nodes } from '../operations.js';
+import { create_fragment_from_html } from '../reconciler.js';
/**
* @param {Element | Text | Comment} anchor
@@ -33,20 +13,14 @@ function remove_from_parent_effect(effect, to_remove) {
* @returns {void}
*/
export function html(anchor, get_value, svg, mathml) {
- const parent_effect = anchor.parentNode !== current_effect?.dom ? current_effect : null;
let value = derived(get_value);
render_effect(() => {
- var dom = html_to_dom(anchor, get(value), svg, mathml);
+ var [start, end] = html_to_dom(anchor, get(value), svg, mathml);
- if (dom) {
- return () => {
- if (parent_effect !== null) {
- remove_from_parent_effect(parent_effect, is_array(dom) ? dom : [dom]);
- }
- remove(dom);
- };
- }
+ return () => {
+ remove_nodes(start, end);
+ };
});
}
@@ -58,10 +32,12 @@ export function html(anchor, get_value, svg, mathml) {
* @param {V} value
* @param {boolean} svg
* @param {boolean} mathml
- * @returns {Element | Comment | (Element | Comment | Text)[]}
+ * @returns {[import('#client').TemplateNode, import('#client').TemplateNode]}
*/
function html_to_dom(target, value, svg, mathml) {
- if (hydrating) return hydrate_nodes;
+ if (hydrating) {
+ return [hydrate_start, hydrate_start];
+ }
var html = value + '';
if (svg) html = ``;
@@ -79,10 +55,11 @@ function html_to_dom(target, value, svg, mathml) {
if (node.childNodes.length === 1) {
var child = /** @type {Text | Element | Comment} */ (node.firstChild);
target.before(child);
- return child;
+ return [child, child];
}
- var nodes = /** @type {Array} */ ([...node.childNodes]);
+ var first = /** @type {import('#client').TemplateNode} */ (node.firstChild);
+ var last = /** @type {import('#client').TemplateNode} */ (node.lastChild);
if (svg || mathml) {
while (node.firstChild) {
@@ -92,5 +69,5 @@ function html_to_dom(target, value, svg, mathml) {
target.before(node);
}
- return nodes;
+ return [first, last];
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js
index 80ed8c09f4f6..41c26bbfd8b0 100644
--- a/packages/svelte/src/internal/client/dom/blocks/if.js
+++ b/packages/svelte/src/internal/client/dom/blocks/if.js
@@ -1,6 +1,5 @@
import { EFFECT_TRANSPARENT } from '../../constants.js';
-import { hydrate_nodes, hydrating, set_hydrating } from '../hydration.js';
-import { remove } from '../reconciler.js';
+import { hydrating, remove_hydrate_nodes, set_hydrating } from '../hydration.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { HYDRATION_END_ELSE } from '../../../../constants.js';
@@ -42,7 +41,7 @@ export function if_block(
if (condition === is_else) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen with `{#if browser}...{/if}`, for example
- remove(hydrate_nodes);
+ remove_hydrate_nodes();
set_hydrating(false);
mismatch = true;
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
index b00a3a242b1e..e41dfc120b57 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
@@ -1,7 +1,7 @@
-import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrate_nodes } from '../hydration.js';
+import { hydrate_anchor, hydrate_start, hydrating, set_hydrate_nodes } from '../hydration.js';
import { empty } from '../operations.js';
import { block } from '../../reactivity/effects.js';
-import { HYDRATION_END, HYDRATION_START } from '../../../../constants.js';
+import { HYDRATION_START } from '../../../../constants.js';
/**
* @type {Node | undefined}
@@ -19,14 +19,14 @@ export function reset_head_anchor() {
export function head(render_fn) {
// The head function may be called after the first hydration pass and ssr comment nodes may still be present,
// therefore we need to skip that when we detect that we're not in hydration mode.
- let previous_hydrate_nodes = null;
+ let previous_hydrate_start = null;
let was_hydrating = hydrating;
/** @type {Comment | Text} */
var anchor;
if (hydrating) {
- previous_hydrate_nodes = hydrate_nodes;
+ previous_hydrate_start = hydrate_start;
// There might be multiple head blocks in our app, so we need to account for each one needing independent hydration.
if (head_anchor === undefined) {
@@ -50,7 +50,7 @@ export function head(render_fn) {
block(() => render_fn(anchor));
} finally {
if (was_hydrating) {
- set_hydrate_nodes(/** @type {import('#client').TemplateNode[]} */ (previous_hydrate_nodes));
+ set_hydrate_nodes(/** @type {import('#client').TemplateNode} */ (previous_hydrate_start));
}
}
}
diff --git a/packages/svelte/src/internal/client/dom/hydration.js b/packages/svelte/src/internal/client/dom/hydration.js
index 06113ba784da..d51ae78ca362 100644
--- a/packages/svelte/src/internal/client/dom/hydration.js
+++ b/packages/svelte/src/internal/client/dom/hydration.js
@@ -1,6 +1,7 @@
import { DEV } from 'esm-env';
import { HYDRATION_END, HYDRATION_START, HYDRATION_ERROR } from '../../../constants.js';
import * as w from '../warnings.js';
+import { remove_nodes } from './operations.js';
/**
* Use this variable to guard everything related to hydration code so it can be treeshaken out
@@ -13,47 +14,36 @@ export function set_hydrating(value) {
hydrating = value;
}
-/**
- * Array of nodes to traverse for hydration. This will be null if we're not hydrating, but for
- * the sake of simplicity we're not going to use `null` checks everywhere and instead rely on
- * the `hydrating` flag to tell whether or not we're in hydration mode at which point this is set.
- * @type {import('#client').TemplateNode[]}
- */
-export let hydrate_nodes = /** @type {any} */ (null);
-
/** @type {import('#client').TemplateNode} */
-export let hydrate_start;
+export let hydrate_start = /** @type {any} */ (null);
-/** @param {import('#client').TemplateNode[]} nodes */
-export function set_hydrate_nodes(nodes) {
- hydrate_nodes = nodes;
- hydrate_start = nodes && nodes[0];
+/**
+ * @param {import('#client').TemplateNode} start
+ */
+export function set_hydrate_nodes(start) {
+ hydrate_start = start;
}
/**
* This function is only called when `hydrating` is true. If passed a `` opening
- * hydration marker, it finds the corresponding closing marker and sets `hydrate_nodes`
- * to everything between the markers, before returning the closing marker.
+ * hydration marker, it sets `hydrate_start` to be the next node and returns the closing marker
* @param {Node} node
* @returns {Node}
*/
export function hydrate_anchor(node) {
- if (node.nodeType !== 8) {
- return node;
- }
-
- var current = /** @type {Node | null} */ (node);
-
// TODO this could have false positives, if a user comment consisted of `[`. need to tighten that up
- if (/** @type {Comment} */ (current).data !== HYDRATION_START) {
+ if (node.nodeType !== 8 || /** @type {Comment} */ (node).data !== HYDRATION_START) {
return node;
}
- /** @type {Node[]} */
- var nodes = [];
+ hydrate_start = /** @type {import('#client').TemplateNode} */ (
+ /** @type {Comment} */ (node).nextSibling
+ );
+
+ var current = hydrate_start;
var depth = 0;
- while ((current = /** @type {Node} */ (current).nextSibling) !== null) {
+ while (current !== null) {
if (current.nodeType === 8) {
var data = /** @type {Comment} */ (current).data;
@@ -61,8 +51,6 @@ export function hydrate_anchor(node) {
depth += 1;
} else if (data[0] === HYDRATION_END) {
if (depth === 0) {
- hydrate_nodes = /** @type {import('#client').TemplateNode[]} */ (nodes);
- hydrate_start = /** @type {import('#client').TemplateNode} */ (nodes[0]);
return current;
}
@@ -70,7 +58,7 @@ export function hydrate_anchor(node) {
}
}
- nodes.push(current);
+ current = /** @type {import('#client').TemplateNode} */ (current.nextSibling);
}
let location;
@@ -86,3 +74,26 @@ export function hydrate_anchor(node) {
w.hydration_mismatch(location);
throw HYDRATION_ERROR;
}
+
+export function remove_hydrate_nodes() {
+ /** @type {import('#client').TemplateNode | null} */
+ var node = hydrate_start;
+ var depth = 0;
+
+ while (node) {
+ if (node.nodeType === 8) {
+ var data = /** @type {Comment} */ (node).data;
+
+ if (data === HYDRATION_START) {
+ depth += 1;
+ } else if (data[0] === HYDRATION_END) {
+ if (depth === 0) return;
+ depth -= 1;
+ }
+ }
+
+ var next = /** @type {import('#client').TemplateNode | null} */ (node.nextSibling);
+ node.remove();
+ node = next;
+ }
+}
diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js
index 508ea47388e1..7fb84aa44c88 100644
--- a/packages/svelte/src/internal/client/dom/operations.js
+++ b/packages/svelte/src/internal/client/dom/operations.js
@@ -1,7 +1,6 @@
import { hydrate_anchor, hydrate_start, hydrating } from './hydration.js';
import { DEV } from 'esm-env';
import { init_array_prototype_warnings } from '../dev/equality.js';
-import { current_effect } from '../runtime.js';
// export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */
@@ -69,7 +68,7 @@ export function child(node) {
}
/**
- * @param {DocumentFragment | import('#client').TemplateNode[]} fragment
+ * @param {DocumentFragment} fragment
* @param {boolean} is_text
* @returns {Node | null}
*/
@@ -83,14 +82,8 @@ export function first_child(fragment, is_text) {
// if an {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
if (is_text && hydrate_start?.nodeType !== 3) {
- var text = empty();
- var dom = /** @type {import('#client').TemplateNode[]} */ (
- /** @type {import('#client').Effect} */ (current_effect).dom
- );
-
- dom.unshift(text);
+ const text = empty();
hydrate_start?.before(text);
-
return text;
}
@@ -114,14 +107,8 @@ export function sibling(node, is_text = false) {
// if a sibling {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
if (is_text && next_sibling?.nodeType !== 3) {
- var text = empty();
- var dom = /** @type {import('#client').TemplateNode[]} */ (
- /** @type {import('#client').Effect} */ (current_effect).dom
- );
-
- dom.unshift(text);
+ const text = empty();
next_sibling?.before(text);
-
return text;
}
@@ -142,3 +129,21 @@ export function clear_text_content(node) {
export function create_element(name) {
return document.createElement(name);
}
+
+/**
+ * Remove all nodes between `from` and `to`, inclusive
+ * @param {import('#client').TemplateNode} from
+ * @param {import('#client').TemplateNode} to
+ */
+export function remove_nodes(from, to) {
+ var node = from;
+
+ while (node) {
+ var next = node.nextSibling;
+
+ node.remove();
+ if (node === to) break;
+
+ node = /** @type {import('#client').TemplateNode} */ (next);
+ }
+}
diff --git a/packages/svelte/src/internal/client/dom/reconciler.js b/packages/svelte/src/internal/client/dom/reconciler.js
index 5b9f246ed338..de9b2db49d83 100644
--- a/packages/svelte/src/internal/client/dom/reconciler.js
+++ b/packages/svelte/src/internal/client/dom/reconciler.js
@@ -6,19 +6,3 @@ export function create_fragment_from_html(html) {
elem.innerHTML = html;
return elem.content;
}
-
-/**
- * @param {import('#client').Dom} current
- */
-export function remove(current) {
- if (is_array(current)) {
- for (var i = 0; i < current.length; i++) {
- var node = current[i];
- if (node.isConnected) {
- node.remove();
- }
- }
- } else if (current.isConnected) {
- current.remove();
- }
-}
diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js
index 6458dc65cddf..73c4e98be9bb 100644
--- a/packages/svelte/src/internal/client/dom/template.js
+++ b/packages/svelte/src/internal/client/dom/template.js
@@ -1,4 +1,4 @@
-import { hydrate_nodes, hydrate_start, hydrating } from './hydration.js';
+import { hydrate_start, hydrating } from './hydration.js';
import { empty } from './operations.js';
import { create_fragment_from_html } from './reconciler.js';
import { current_effect } from '../runtime.js';
@@ -6,14 +6,14 @@ import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.
import { effect } from '../reactivity/effects.js';
/**
- * @template {import("#client").TemplateNode | import("#client").TemplateNode[]} T
- * @param {T} dom
+ * @template {import("#client").TemplateNode} T
+ * @param {T} d1
*/
-function push_template_node(dom) {
+function push_template_node(d1) {
var effect = /** @type {import('#client').Effect} */ (current_effect);
- if (effect.dom === null) {
- effect.dom = dom;
+ if (effect.d1 === null) {
+ effect.d1 = d1;
}
}
@@ -32,7 +32,7 @@ export function template(content, flags) {
return () => {
if (hydrating) {
- push_template_node(is_fragment ? hydrate_nodes : hydrate_start);
+ push_template_node(hydrate_start);
return hydrate_start;
}
@@ -77,7 +77,6 @@ export function template_with_script(content, flags) {
*/
/*#__NO_SIDE_EFFECTS__*/
export function ns_template(content, flags, ns = 'svg') {
- var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0;
var fn = template(`<${ns}>${content}${ns}>`, 0); // we don't need to worry about using importNode for namespaced elements
/** @type {Element | DocumentFragment} */
@@ -85,7 +84,7 @@ export function ns_template(content, flags, ns = 'svg') {
return () => {
if (hydrating) {
- push_template_node(is_fragment ? hydrate_nodes : hydrate_start);
+ push_template_node(hydrate_start);
return hydrate_start;
}
@@ -182,7 +181,7 @@ export function text(anchor) {
var node = hydrate_start;
if (!node) {
- // if an {expression} is empty during SSR, `hydrate_nodes` will be empty.
+ // if an {expression} is empty during SSR, `hydrate_start` will be missing.
// we need to insert an empty text node
anchor.before((node = empty()));
}
@@ -194,7 +193,7 @@ export function text(anchor) {
export function comment() {
// we're not delegating to `template` here for performance reasons
if (hydrating) {
- push_template_node(hydrate_nodes);
+ push_template_node(hydrate_start);
return hydrate_start;
}
@@ -212,14 +211,21 @@ export function comment() {
* @param {DocumentFragment | Element} dom
*/
export function append(anchor, dom) {
- if (hydrating) return;
-
var effect = /** @type {import('#client').Effect} */ (current_effect);
- effect.dom =
- dom.nodeType === 11
- ? /** @type {import('#client').TemplateNode[]} */ ([...dom.childNodes])
- : /** @type {Element | Comment} */ (dom);
+ if (!hydrating) {
+ if (dom.nodeType === 11) {
+ // prepend an empty text node
+ var d1 = empty();
+
+ /** @type {import('#client').TemplateNode} */ (dom.firstChild).before(d1);
+ effect.d1 = d1;
+ } else {
+ effect.d1 = /** @type {Element} */ (dom);
+ }
+
+ anchor.before(/** @type {Node} */ (dom));
+ }
- anchor.before(/** @type {Node} */ (dom));
+ effect.d2 = anchor?.previousSibling;
}
diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js
index 6411e8109af7..0554271fc44b 100644
--- a/packages/svelte/src/internal/client/reactivity/effects.js
+++ b/packages/svelte/src/internal/client/reactivity/effects.js
@@ -33,10 +33,10 @@ import {
UNOWNED
} from '../constants.js';
import { set } from './sources.js';
-import { remove } from '../dom/reconciler.js';
import * as e from '../errors.js';
import { DEV } from 'esm-env';
import { define_property } from '../utils.js';
+import { remove_nodes } from '../dom/operations.js';
/**
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
@@ -79,7 +79,8 @@ function create_effect(type, fn, sync) {
var effect = {
ctx: current_component_context,
deps: null,
- dom: null,
+ d1: null,
+ d2: null,
f: type | DIRTY,
first: null,
fn,
@@ -314,10 +315,11 @@ export function execute_effect_teardown(effect) {
* @returns {void}
*/
export function destroy_effect(effect) {
- var dom = effect.dom;
+ var d1 = effect.d1;
+ var d2 = effect.d2;
- if (dom !== null) {
- remove(dom);
+ if (d1 !== null && d2 !== null) {
+ remove_nodes(d1, d2);
}
destroy_effect_children(effect);
@@ -360,7 +362,8 @@ export function destroy_effect(effect) {
effect.prev =
effect.teardown =
effect.ctx =
- effect.dom =
+ effect.d1 =
+ effect.d2 =
effect.deps =
effect.parent =
// @ts-expect-error
diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts
index 7502443bd10c..eef3c3a2e3e2 100644
--- a/packages/svelte/src/internal/client/reactivity/types.d.ts
+++ b/packages/svelte/src/internal/client/reactivity/types.d.ts
@@ -1,4 +1,4 @@
-import type { ComponentContext, Dom, Equals, TransitionManager } from '#client';
+import type { ComponentContext, Dom, Equals, TemplateNode, TransitionManager } from '#client';
export interface Signal {
/** Flags bitmask */
@@ -36,7 +36,8 @@ export interface Derived extends Value, Reaction {
export interface Effect extends Reaction {
parent: Effect | null;
- dom: Dom | null;
+ d1: TemplateNode | null;
+ d2: TemplateNode | null;
/** The associated component context */
ctx: null | ComponentContext;
/** The effect function */
diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js
index 101ba6f1afd3..2117113c555d 100644
--- a/packages/svelte/src/internal/client/render.js
+++ b/packages/svelte/src/internal/client/render.js
@@ -5,7 +5,7 @@ import { flush_sync, push, pop, current_component_context } from './runtime.js';
import { effect_root, branch } from './reactivity/effects.js';
import {
hydrate_anchor,
- hydrate_nodes,
+ hydrate_start,
hydrating,
set_hydrate_nodes,
set_hydrating
@@ -129,7 +129,7 @@ export function hydrate(component, options) {
}
const target = options.target;
- const previous_hydrate_nodes = hydrate_nodes;
+ const previous_hydrate_start = hydrate_start;
try {
// Don't flush previous effects to ensure order of outer effects stays consistent
@@ -173,8 +173,8 @@ export function hydrate(component, options) {
throw error;
} finally {
- set_hydrating(!!previous_hydrate_nodes);
- set_hydrate_nodes(previous_hydrate_nodes);
+ set_hydrating(!!previous_hydrate_start);
+ set_hydrate_nodes(previous_hydrate_start);
reset_head_anchor();
}
}