Skip to content

Commit c83aa06

Browse files
authored
chore: proactively defer effects in pending boundary (#17734)
Currently, (render/template) effects inside pending boundaries are deferred, but in an indirect manner: first we schedule them, then we `flush` the current batch, and in the course of traversing the effect tree we find any dirty effects and defer them at the level of the topmost pending boundary. This doesn't really make sense — we can just skip to the end state and skip the scheduling/traversal, since the effects don't become relevant until the boundary resolves. This PR implements that. It is a stepping stone towards a larger refactor, in which scheduling becomes batch-centric and lazier. While it shouldn't change any observable behaviour, I've added a changeset out of an abundance of caution. ### Before submitting the PR, please make sure you do the following - [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`. - [x] This message body should clearly illustrate what problems it solves. - [ ] Ideally, include a test that fails without this PR but passes with it. - [x] If this PR changes code within `packages/svelte/src`, add a changeset (`npx changeset`). ### Tests and linting - [x] Run the tests with `pnpm test` and lint the project with `pnpm lint`
1 parent 2287ad0 commit c83aa06

File tree

3 files changed

+50
-48
lines changed

3 files changed

+50
-48
lines changed

.changeset/angry-ideas-listen.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
chore: proactively defer effects in pending boundary

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

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/** @import { Effect, Source, TemplateNode, } from '#client' */
22
import {
33
BOUNDARY_EFFECT,
4-
COMMENT_NODE,
54
DIRTY,
65
EFFECT_PRESERVED,
76
EFFECT_TRANSPARENT,
@@ -202,7 +201,7 @@ export class Boundary {
202201
this.#pending_effect = null;
203202
});
204203

205-
this.is_pending = false;
204+
this.#resolve();
206205
}
207206
});
208207
}
@@ -224,13 +223,33 @@ export class Boundary {
224223
const pending = /** @type {(anchor: Node) => void} */ (this.#props.pending);
225224
this.#pending_effect = branch(() => pending(this.#anchor));
226225
} else {
227-
this.is_pending = false;
226+
this.#resolve();
228227
}
229228
} catch (error) {
230229
this.error(error);
231230
}
232231
}
233232

233+
#resolve() {
234+
this.is_pending = false;
235+
236+
// any effects that were previously deferred should be rescheduled —
237+
// after the next traversal (which will happen immediately, due to the
238+
// same update that brought us here) the effects will be flushed
239+
for (const e of this.#dirty_effects) {
240+
set_signal_status(e, DIRTY);
241+
schedule_effect(e);
242+
}
243+
244+
for (const e of this.#maybe_dirty_effects) {
245+
set_signal_status(e, MAYBE_DIRTY);
246+
schedule_effect(e);
247+
}
248+
249+
this.#dirty_effects.clear();
250+
this.#maybe_dirty_effects.clear();
251+
}
252+
234253
/**
235254
* Defer an effect inside a pending boundary until the boundary resolves
236255
* @param {Effect} effect
@@ -294,24 +313,7 @@ export class Boundary {
294313
this.#pending_count += d;
295314

296315
if (this.#pending_count === 0) {
297-
this.is_pending = false;
298-
299-
// any effects that were encountered and deferred during traversal
300-
// should be rescheduled — after the next traversal (which will happen
301-
// immediately, due to the same update that brought us here)
302-
// the effects will be flushed
303-
for (const e of this.#dirty_effects) {
304-
set_signal_status(e, DIRTY);
305-
schedule_effect(e);
306-
}
307-
308-
for (const e of this.#maybe_dirty_effects) {
309-
set_signal_status(e, MAYBE_DIRTY);
310-
schedule_effect(e);
311-
}
312-
313-
this.#dirty_effects.clear();
314-
this.#maybe_dirty_effects.clear();
316+
this.#resolve();
315317

316318
if (this.#pending_effect) {
317319
pause_effect(this.#pending_effect, () => {

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

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import {
1818
EAGER_EFFECT,
1919
HEAD_EFFECT,
2020
ERROR_VALUE,
21-
MANAGED_EFFECT
21+
MANAGED_EFFECT,
22+
REACTION_RAN
2223
} from '#client/constants';
2324
import { async_mode_flag } from '../../flags/index.js';
2425
import { deferred, define_property, includes } from '../../shared/utils.js';
@@ -246,36 +247,16 @@ export class Batch {
246247

247248
var effect = root.first;
248249

249-
/** @type {Effect | null} */
250-
var pending_boundary = null;
251-
252250
while (effect !== null) {
253251
var flags = effect.f;
254252
var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0;
255253
var is_skippable_branch = is_branch && (flags & CLEAN) !== 0;
256254

257255
var skip = is_skippable_branch || (flags & INERT) !== 0 || this.#skipped_branches.has(effect);
258256

259-
// Inside a `<svelte:boundary>` with a pending snippet,
260-
// all effects are deferred until the boundary resolves
261-
// (except block/async effects, which run immediately)
262-
if (
263-
async_mode_flag &&
264-
pending_boundary === null &&
265-
(flags & BOUNDARY_EFFECT) !== 0 &&
266-
effect.b?.is_pending
267-
) {
268-
pending_boundary = effect;
269-
}
270-
271257
if (!skip && effect.fn !== null) {
272258
if (is_branch) {
273259
effect.f ^= CLEAN;
274-
} else if (
275-
pending_boundary !== null &&
276-
(flags & (EFFECT | RENDER_EFFECT | MANAGED_EFFECT)) !== 0
277-
) {
278-
/** @type {Boundary} */ (pending_boundary.b).defer_effect(effect);
279260
} else if ((flags & EFFECT) !== 0) {
280261
effects.push(effect);
281262
} else if (async_mode_flag && (flags & (RENDER_EFFECT | MANAGED_EFFECT)) !== 0) {
@@ -294,10 +275,6 @@ export class Batch {
294275
}
295276

296277
while (effect !== null) {
297-
if (effect === pending_boundary) {
298-
pending_boundary = null;
299-
}
300-
301278
var next = effect.next;
302279

303280
if (next !== null) {
@@ -839,6 +816,19 @@ function depends_on(reaction, sources, checked) {
839816
export function schedule_effect(signal) {
840817
var effect = (last_scheduled_effect = signal);
841818

819+
var boundary = effect.b;
820+
821+
// defer render effects inside a pending boundary
822+
// TODO the `REACTION_RAN` check is only necessary because of legacy `$:` effects AFAICT — we can remove later
823+
if (
824+
boundary?.is_pending &&
825+
(signal.f & (EFFECT | RENDER_EFFECT | MANAGED_EFFECT)) !== 0 &&
826+
(signal.f & REACTION_RAN) === 0
827+
) {
828+
boundary.defer_effect(signal);
829+
return;
830+
}
831+
842832
while (effect.parent !== null) {
843833
effect = effect.parent;
844834
var flags = effect.f;
@@ -850,13 +840,18 @@ export function schedule_effect(signal) {
850840
is_flushing &&
851841
effect === active_effect &&
852842
(flags & BLOCK_EFFECT) !== 0 &&
853-
(flags & HEAD_EFFECT) === 0
843+
(flags & HEAD_EFFECT) === 0 &&
844+
(flags & REACTION_RAN) !== 0
854845
) {
855846
return;
856847
}
857848

858849
if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) {
859-
if ((flags & CLEAN) === 0) return;
850+
if ((flags & CLEAN) === 0) {
851+
// branch is already dirty, bail
852+
return;
853+
}
854+
860855
effect.f ^= CLEAN;
861856
}
862857
}

0 commit comments

Comments
 (0)