Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/slimy-gifts-cough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

perf: skip repeatedly traversing the same derived
15 changes: 13 additions & 2 deletions packages/svelte/src/internal/client/constants.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
// General flags
export const DERIVED = 1 << 1;
export const EFFECT = 1 << 2;
export const RENDER_EFFECT = 1 << 3;
export const BLOCK_EFFECT = 1 << 4;
export const BRANCH_EFFECT = 1 << 5;
export const ROOT_EFFECT = 1 << 6;
export const BOUNDARY_EFFECT = 1 << 7;
export const UNOWNED = 1 << 8;
export const DISCONNECTED = 1 << 9;
export const CLEAN = 1 << 10;
export const DIRTY = 1 << 11;
export const MAYBE_DIRTY = 1 << 12;
export const INERT = 1 << 13;
export const DESTROYED = 1 << 14;

// Flags exclusive to effects
export const EFFECT_RAN = 1 << 15;
/** 'Transparent' effects do not create a transition boundary */
export const EFFECT_TRANSPARENT = 1 << 16;
Expand All @@ -20,6 +21,16 @@ export const HEAD_EFFECT = 1 << 18;
export const EFFECT_PRESERVED = 1 << 19;
export const USER_EFFECT = 1 << 20;

// Flags exclusive to deriveds
export const UNOWNED = 1 << 8;
export const DISCONNECTED = 1 << 9;
/**
* Tells that we marked this derived and its reactions as visited during the "mark as (maybe) dirty"-phase.
* Will be lifted during execution of the derived and during checking its dirty state (both are necessary
* because a derived might be checked but not executed).
*/
export const WAS_MARKED = 1 << 15;

// Flags used for async
export const REACTION_IS_UPDATING = 1 << 21;
export const ASYNC = 1 << 22;
Expand Down
32 changes: 26 additions & 6 deletions packages/svelte/src/internal/client/reactivity/batch.js
Original file line number Diff line number Diff line change
Expand Up @@ -377,8 +377,12 @@ export class Batch {
// Re-run async/block effects that depend on distinct values changed in both batches
const others = [...batch.current.keys()].filter((s) => !this.current.has(s));
if (others.length > 0) {
/** @type {Set<Value>} */
const marked = new Set();
/** @type {Map<Reaction, boolean>} */
const checked = new Map();
for (const source of sources) {
mark_effects(source, others);
mark_effects(source, others, marked, checked);
}

if (queued_root_effects.length > 0) {
Expand Down Expand Up @@ -688,15 +692,24 @@ function flush_queued_effects(effects) {
* these effects can re-run after another batch has been committed
* @param {Value} value
* @param {Source[]} sources
* @param {Set<Value>} marked
* @param {Map<Reaction, boolean>} checked
*/
function mark_effects(value, sources) {
function mark_effects(value, sources, marked, checked) {
if (marked.has(value)) return;
marked.add(value);

if (value.reactions !== null) {
for (const reaction of value.reactions) {
const flags = reaction.f;

if ((flags & DERIVED) !== 0) {
mark_effects(/** @type {Derived} */ (reaction), sources);
} else if ((flags & (ASYNC | BLOCK_EFFECT)) !== 0 && depends_on(reaction, sources)) {
mark_effects(/** @type {Derived} */ (reaction), sources, marked, checked);
} else if (
(flags & (ASYNC | BLOCK_EFFECT)) !== 0 &&
(flags & DIRTY) === 0 && // we may have scheduled this one already
depends_on(reaction, sources, checked)
) {
set_signal_status(reaction, DIRTY);
schedule_effect(/** @type {Effect} */ (reaction));
}
Expand All @@ -707,20 +720,27 @@ function mark_effects(value, sources) {
/**
* @param {Reaction} reaction
* @param {Source[]} sources
* @param {Map<Reaction, boolean>} checked
*/
function depends_on(reaction, sources) {
function depends_on(reaction, sources, checked) {
const depends = checked.get(reaction);
if (depends !== undefined) return depends;

if (reaction.deps !== null) {
for (const dep of reaction.deps) {
if (sources.includes(dep)) {
return true;
}

if ((dep.f & DERIVED) !== 0 && depends_on(/** @type {Derived} */ (dep), sources)) {
if ((dep.f & DERIVED) !== 0 && depends_on(/** @type {Derived} */ (dep), sources, checked)) {
checked.set(/** @type {Derived} */ (dep), true);
return true;
}
}
}

checked.set(reaction, false);

return false;
}

Expand Down
5 changes: 4 additions & 1 deletion packages/svelte/src/internal/client/reactivity/deriveds.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
MAYBE_DIRTY,
STALE_REACTION,
UNOWNED,
ASYNC
ASYNC,
WAS_MARKED
} from '#client/constants';
import {
active_reaction,
Expand Down Expand Up @@ -326,6 +327,7 @@ export function execute_derived(derived) {

stack.push(derived);

derived.f &= ~WAS_MARKED;
destroy_derived_effects(derived);
value = update_reaction(derived);
} finally {
Expand All @@ -335,6 +337,7 @@ export function execute_derived(derived) {
}
} else {
try {
derived.f &= ~WAS_MARKED;
destroy_derived_effects(derived);
value = update_reaction(derived);
} finally {
Expand Down
8 changes: 6 additions & 2 deletions packages/svelte/src/internal/client/reactivity/sources.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import {
MAYBE_DIRTY,
BLOCK_EFFECT,
ROOT_EFFECT,
ASYNC
ASYNC,
WAS_MARKED
} from '#client/constants';
import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
Expand Down Expand Up @@ -332,7 +333,10 @@ function mark_reactions(signal, status) {
}

if ((flags & DERIVED) !== 0) {
mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
if ((flags & WAS_MARKED) === 0) {
reaction.f |= WAS_MARKED;
mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
}
} else if (not_dirty) {
if ((flags & BLOCK_EFFECT) !== 0) {
if (eager_block_effects !== null) {
Expand Down
7 changes: 6 additions & 1 deletion packages/svelte/src/internal/client/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
DISCONNECTED,
REACTION_IS_UPDATING,
STALE_REACTION,
ERROR_VALUE
ERROR_VALUE,
WAS_MARKED
} from './constants.js';
import { old_values } from './reactivity/sources.js';
import {
Expand Down Expand Up @@ -161,6 +162,10 @@ export function is_dirty(reaction) {
var dependencies = reaction.deps;
var is_unowned = (flags & UNOWNED) !== 0;

if (flags & DERIVED) {
reaction.f &= ~WAS_MARKED;
}

if (dependencies !== null) {
var i;
var dependency;
Expand Down
Loading