Skip to content

Commit 02441c6

Browse files
authored
chore: apply each block controlled teardown optimization, again (#11051)
1 parent f7c8fd5 commit 02441c6

File tree

2 files changed

+54
-38
lines changed

2 files changed

+54
-38
lines changed

packages/svelte/src/internal/client/dom/blocks/each.js

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ import { untrack } from '../../runtime.js';
1515
import {
1616
block,
1717
branch,
18+
destroy_effect,
1819
effect,
20+
run_out_transitions,
21+
pause_children,
1922
pause_effect,
20-
pause_effects,
2123
resume_effect
2224
} from '../../reactivity/effects.js';
2325
import { source, mutable_source, set } from '../../reactivity/sources.js';
@@ -39,6 +41,39 @@ export function set_current_each_item(item) {
3941
current_each_item = item;
4042
}
4143

44+
/**
45+
* Pause multiple effects simultaneously, and coordinate their
46+
* subsequent destruction. Used in each blocks
47+
* @param {import('#client').Effect[]} effects
48+
* @param {null | Node} controlled_anchor
49+
* @param {() => void} [callback]
50+
*/
51+
function pause_effects(effects, controlled_anchor, callback) {
52+
/** @type {import('#client').TransitionManager[]} */
53+
var transitions = [];
54+
var length = effects.length;
55+
56+
for (var i = 0; i < length; i++) {
57+
pause_children(effects[i], transitions, true);
58+
}
59+
60+
// If we have a controlled anchor, it means that the each block is inside a single
61+
// DOM element, so we can apply a fast-path for clearing the contents of the element.
62+
if (effects.length > 0 && transitions.length === 0 && controlled_anchor !== null) {
63+
var parent_node = /** @type {Element} */ (controlled_anchor.parentNode);
64+
parent_node.textContent = '';
65+
parent_node.append(controlled_anchor);
66+
}
67+
68+
run_out_transitions(transitions, () => {
69+
for (var i = 0; i < length; i++) {
70+
destroy_effect(effects[i]);
71+
}
72+
73+
if (callback !== undefined) callback();
74+
});
75+
}
76+
4277
/**
4378
* @template V
4479
* @param {Element | Comment} anchor The next sibling node, or the parent node if this is a 'controlled' block
@@ -145,7 +180,6 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
145180
}
146181

147182
if (!hydrating) {
148-
// TODO add 'empty controlled block' optimisation here
149183
reconcile_fn(array, state, anchor, render_fn, flags, keys);
150184
}
151185

@@ -244,7 +278,9 @@ function reconcile_indexed_array(array, state, anchor, render_fn, flags) {
244278
effects.push(a_items[i].e);
245279
}
246280

247-
pause_effects(effects, () => {
281+
var controlled_anchor = (flags & EACH_IS_CONTROLLED) !== 0 && b === 0 ? anchor : null;
282+
283+
pause_effects(effects, controlled_anchor, () => {
248284
state.items.length = b;
249285
});
250286
}
@@ -274,6 +310,7 @@ function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) {
274310

275311
var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
276312
var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
313+
var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
277314
var start = 0;
278315
var item;
279316

@@ -381,6 +418,11 @@ function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) {
381418
// I fully understand this part)
382419
if (moved) {
383420
mark_lis(sources);
421+
} else if (is_controlled && to_destroy.length === a_items.length) {
422+
// We can optimize the case in which all items are replaced —
423+
// destroy everything first, then append new items
424+
pause_effects(to_destroy, anchor);
425+
to_destroy = [];
384426
}
385427

386428
// working from the back, insert new or moved items
@@ -421,9 +463,9 @@ function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) {
421463
});
422464
}
423465

424-
// TODO: would be good to avoid this closure in the case where we have no
425-
// transitions at all. It would make it far more JIT friendly in the hot cases.
426-
pause_effects(to_destroy, () => {
466+
var controlled_anchor = is_controlled && b_items.length === 0 ? anchor : null;
467+
468+
pause_effects(to_destroy, controlled_anchor, () => {
427469
state.items = b_items;
428470
});
429471
}

packages/svelte/src/internal/client/reactivity/effects.js

Lines changed: 6 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import {
2727
IS_ELSEIF
2828
} from '../constants.js';
2929
import { set } from './sources.js';
30-
import { noop } from '../../shared/utils.js';
3130
import { remove } from '../dom/reconciler.js';
3231

3332
/**
@@ -295,50 +294,25 @@ export function destroy_effect(effect) {
295294
* completed, and if the state change is reversed then we _resume_ it.
296295
* A paused effect does not update, and the DOM subtree becomes inert.
297296
* @param {import('#client').Effect} effect
298-
* @param {() => void} callback
297+
* @param {() => void} [callback]
299298
*/
300-
export function pause_effect(effect, callback = noop) {
299+
export function pause_effect(effect, callback) {
301300
/** @type {import('#client').TransitionManager[]} */
302301
var transitions = [];
303302

304303
pause_children(effect, transitions, true);
305304

306-
out(transitions, () => {
305+
run_out_transitions(transitions, () => {
307306
destroy_effect(effect);
308-
callback();
309-
});
310-
}
311-
312-
/**
313-
* Pause multiple effects simultaneously, and coordinate their
314-
* subsequent destruction. Used in each blocks
315-
* @param {import('#client').Effect[]} effects
316-
* @param {() => void} callback
317-
*/
318-
export function pause_effects(effects, callback = noop) {
319-
/** @type {import('#client').TransitionManager[]} */
320-
var transitions = [];
321-
var length = effects.length;
322-
323-
for (var i = 0; i < length; i++) {
324-
pause_children(effects[i], transitions, true);
325-
}
326-
327-
// TODO: would be good to avoid this closure in the case where we have no
328-
// transitions at all. It would make it far more JIT friendly in the hot cases.
329-
out(transitions, () => {
330-
for (var i = 0; i < length; i++) {
331-
destroy_effect(effects[i]);
332-
}
333-
callback();
307+
if (callback) callback();
334308
});
335309
}
336310

337311
/**
338312
* @param {import('#client').TransitionManager[]} transitions
339313
* @param {() => void} fn
340314
*/
341-
function out(transitions, fn) {
315+
export function run_out_transitions(transitions, fn) {
342316
var remaining = transitions.length;
343317
if (remaining > 0) {
344318
var check = () => --remaining || fn();
@@ -355,7 +329,7 @@ function out(transitions, fn) {
355329
* @param {import('#client').TransitionManager[]} transitions
356330
* @param {boolean} local
357331
*/
358-
function pause_children(effect, transitions, local) {
332+
export function pause_children(effect, transitions, local) {
359333
if ((effect.f & INERT) !== 0) return;
360334
effect.f ^= INERT;
361335

0 commit comments

Comments
 (0)