diff --git a/.changeset/hungry-dots-fry.md b/.changeset/hungry-dots-fry.md new file mode 100644 index 0000000000..5c2328f6f9 --- /dev/null +++ b/.changeset/hungry-dots-fry.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: more signal perf tuning diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index c741053724..1e46d79e78 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -624,7 +624,7 @@ export function bind_playback_rate(media, get_value, update) { // Needs to happen after the element is inserted into the dom, else playback will be set back to 1 by the browser. // For hydration we could do it immediately but the additional code is not worth the lost microtask. - /** @type {import('./types.js').Signal | undefined} */ + /** @type {import('./types.js').ComputationSignal | undefined} */ let render; let destroyed = false; const effect = managed_effect(() => { @@ -2125,7 +2125,7 @@ export function destroy_each_item_block( if (!controlled && dom !== null) { remove(dom); } - destroy_signal(/** @type {import('./types.js').Signal} */ (block.effect)); + destroy_signal(/** @type {import('./types.js').EffectSignal} */ (block.effect)); } } @@ -3163,7 +3163,7 @@ export function mount(component, options) { if (hydration_fragment !== null) { remove(hydration_fragment); } - destroy_signal(/** @type {import('./types.js').Signal} */ (block.effect)); + destroy_signal(/** @type {import('./types.js').EffectSignal} */ (block.effect)); } ]; } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 7e31736632..2207ac9507 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -46,7 +46,7 @@ let current_queued_tasks = []; let flush_count = 0; // Handle signal reactivity tree dependencies and consumer -/** @type {null | import('./types.js').Signal} */ +/** @type {null | import('./types.js').ComputationSignal} */ let current_consumer = null; /** @type {null | import('./types.js').EffectSignal} */ @@ -133,14 +133,31 @@ function default_equals(a, b) { return a === b; } +/** + * @template V + * @param {import('./types.js').SignalFlags} flags + * @param {V} value + * @returns {import('./types.js').SourceSignal} + */ +function create_source_signal(flags, value) { + return { + consumers: null, + // We can remove this if we get rid of beforeUpdate/afterUpdate + context: null, + equals: null, + flags, + value + }; +} + /** * @template V * @param {import('./types.js').SignalFlags} flags * @param {V} value * @param {import('./types.js').Block | null} block - * @returns {import('./types.js').Signal} + * @returns {import('./types.js').ComputationSignal} */ -function create_signal_object(flags, value, block) { +function create_computation_signal(flags, value, block) { return { block, consumers: null, @@ -156,8 +173,8 @@ function create_signal_object(flags, value, block) { } /** - * @param {import('./types.js').Signal} target_signal - * @param {import('./types.js').Signal} ref_signal + * @param {import('./types.js').ComputationSignal} target_signal + * @param {import('./types.js').ComputationSignal} ref_signal * @returns {void} */ function push_reference(target_signal, ref_signal) { @@ -180,7 +197,7 @@ function is_signal_dirty(signal) { return true; } if ((flags & MAYBE_DIRTY) !== 0) { - const dependencies = signal.dependencies; + const dependencies = /** @type {import('./types.js').ComputationSignal} **/ (signal).dependencies; if (dependencies !== null) { const length = dependencies.length; let i; @@ -195,7 +212,7 @@ function is_signal_dirty(signal) { // The flags can be marked as dirty from the above is_signal_dirty call. if ((dependency.flags & DIRTY) !== 0) { if ((dep_flags & DERIVED) !== 0) { - update_derived(dependency, true); + update_derived(/** @type {import('./types.js').ComputationSignal} **/ (dependency), true); // Might have been mutated from above get. if ((signal.flags & DIRTY) !== 0) { return true; @@ -212,7 +229,7 @@ function is_signal_dirty(signal) { /** * @template V - * @param {import('./types.js').Signal} signal + * @param {import('./types.js').ComputationSignal} signal * @returns {V} */ function execute_signal_fn(signal) { @@ -247,7 +264,7 @@ function execute_signal_fn(signal) { } else { res = /** @type {() => V} */ (init)(); } - let dependencies = signal.dependencies; + let dependencies = /** @type {import('./types.js').Signal[]} **/ (signal.dependencies); if (current_dependencies !== null) { let i; @@ -259,7 +276,7 @@ function execute_signal_fn(signal) { dependencies[current_dependencies_index + i] = current_dependencies[i]; } } else { - signal.dependencies = dependencies = current_dependencies; + signal.dependencies = /** @type {import('./types.js').Signal[]} **/ (dependencies = current_dependencies); } if (!current_skip_consumer) { @@ -291,7 +308,7 @@ function execute_signal_fn(signal) { /** * @template V - * @param {import('./types.js').Signal} signal + * @param {import('./types.js').ComputationSignal} signal * @param {number} start_index * @param {boolean} remove_unowned * @returns {void} @@ -316,7 +333,11 @@ function remove_consumer(signal, start_index, remove_unowned) { } } if (remove_unowned && consumers_length === 0 && (dependency.flags & UNOWNED) !== 0) { - remove_consumer(dependency, 0, true); + remove_consumer( + /** @type {import('./types.js').ComputationSignal} **/ (dependency), + 0, + true + ); } } } @@ -324,7 +345,7 @@ function remove_consumer(signal, start_index, remove_unowned) { /** * @template V - * @param {import('./types.js').Signal} signal + * @param {import('./types.js').ComputationSignal} signal * @returns {void} */ function destroy_references(signal) { @@ -575,7 +596,7 @@ export async function tick() { /** * @template V - * @param {import('./types.js').Signal} signal + * @param {import('./types.js').ComputationSignal} signal * @param {boolean} force_schedule * @returns {void} */ @@ -615,10 +636,11 @@ export function store_get(store, store_name, stores) { value: source(UNINITIALIZED), unsubscribe: EMPTY_FUNC }; - push_destroy_fn(entry.value, () => { - /** @type {import('./types.js').StoreReferencesContainer['']} */ (entry).last_value = - /** @type {import('./types.js').StoreReferencesContainer['']} */ (entry).value.value; - }); + // TODO: can we remove this code? it was refactored out when we split up source/comptued signals + // push_destroy_fn(entry.value, () => { + // /** @type {import('./types.js').StoreReferencesContainer['']} */ (entry).last_value = + // /** @type {import('./types.js').StoreReferencesContainer['']} */ (entry).value.value; + // }); stores[store_name] = entry; } @@ -676,7 +698,8 @@ export function unsubscribe_on_destroy(stores) { for (store_name in stores) { const ref = stores[store_name]; ref.unsubscribe(); - destroy_signal(ref.value); + // TODO: can we remove this code? it was refactored out when we split up source/comptued signals + // destroy_signal(ref.value); } }); } @@ -740,7 +763,7 @@ export function get(signal) { } if ((flags & DERIVED) !== 0 && is_signal_dirty(signal)) { - update_derived(signal, false); + update_derived(/** @type {import('./types.js').ComputationSignal} **/ (signal), false); } return signal.value; } @@ -845,7 +868,7 @@ export function mutate_store(store, expression, new_value) { } /** - * @param {import('./types.js').Signal} signal + * @param {import('./types.js').ComputationSignal} signal * @param {boolean} inert * @returns {void} */ @@ -969,12 +992,13 @@ export function set_signal_value(signal, value) { /** * @template V - * @param {import('./types.js').Signal} signal + * @param {import('./types.js').ComputationSignal} signal * @returns {void} */ export function destroy_signal(signal) { const teardown = /** @type {null | (() => void)} */ (signal.value); const destroy = signal.destroy; + const flags = signal.flags; destroy_references(signal); remove_consumer(signal, 0, true); signal.init = null; @@ -1005,14 +1029,14 @@ export function destroy_signal(signal) { * @template V * @param {() => V} init * @param {import('./types.js').EqualsFunctions} [equals] - * @returns {import('./types.js').Signal} + * @returns {import('./types.js').ComputationSignal} */ /*#__NO_SIDE_EFFECTS__*/ export function derived(init, equals) { const is_unowned = current_effect === null; const flags = is_unowned ? DERIVED | UNOWNED : DERIVED; - const signal = /** @type {import('./types.js').Signal} */ ( - create_signal_object(flags | CLEAN, UNINITIALIZED, current_block) + const signal = /** @type {import('./types.js').ComputationSignal} */ ( + create_computation_signal(flags | CLEAN, UNINITIALIZED, current_block) ); signal.init = init; signal.context = current_component_context; @@ -1027,11 +1051,11 @@ export function derived(init, equals) { * @template V * @param {V} initial_value * @param {import('./types.js').EqualsFunctions} [equals] - * @returns {import('./types.js').Signal} + * @returns {import('./types.js').SourceSignal} */ /*#__NO_SIDE_EFFECTS__*/ export function source(initial_value, equals) { - const source = create_signal_object(SOURCE | CLEAN, initial_value, null); + const source = create_source_signal(SOURCE | CLEAN, initial_value); source.context = current_component_context; source.equals = get_equals_method(equals); return source; @@ -1079,7 +1103,7 @@ export function untrack(fn) { * @returns {import('./types.js').EffectSignal} */ function internal_create_effect(type, init, sync, block, schedule) { - const signal = create_signal_object(type | DIRTY, null, block); + const signal = create_computation_signal(type | DIRTY, null, block); signal.init = init; signal.context = current_component_context; if (schedule) { @@ -1210,7 +1234,7 @@ export function managed_render_effect(init, block = current_block, sync = true) /** * @template V - * @param {import('./types.js').Signal} signal + * @param {import('./types.js').ComputationSignal} signal * @param {() => void} destroy_fn * @returns {void} */ diff --git a/packages/svelte/src/internal/client/transitions.js b/packages/svelte/src/internal/client/transitions.js index b07db93359..357e287d4e 100644 --- a/packages/svelte/src/internal/client/transitions.js +++ b/packages/svelte/src/internal/client/transitions.js @@ -253,7 +253,7 @@ function handle_raf(time) { * @param {HTMLElement} dom * @param {() => import('./types.js').TransitionPayload} init * @param {'in' | 'out' | 'both' | 'key'} direction - * @param {import('./types.js').Signal} effect + * @param {import('./types.js').EffectSignal} effect * @returns {import('./types.js').Transition} */ function create_transition(dom, init, direction, effect) { diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 5bc4ce730b..fa9806d781 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -46,15 +46,28 @@ export type ComponentContext = { }; }; -export type Signal = { +export type SourceSignal = { + /** Signals that read from the current signal */ + consumers: null | ComputationSignal[]; + /** The associated component if this signal is an effect/computed */ + context: null | ComponentContext; + /** For value equality */ + equals: null | EqualsFunctions; + /** The types that the signal represent, as a bitwise value */ + flags: SignalFlags; + /** The latest value for this signal, doubles as the teardown for effects */ + value: V; +}; + +export type ComputationSignal = { /** The block associated with this effect/computed */ block: null | Block; /** Signals that read from the current signal */ - consumers: null | Signal[]; + consumers: null | ComputationSignal[]; /** The associated component if this signal is an effect/computed */ context: null | ComponentContext; /** Signals that this signal reads from */ - dependencies: null | Signal[]; + dependencies: null | Signal[]; /** Thing(s) that need destroying */ destroy: null | (() => void) | Array<() => void>; /** For value equality */ @@ -64,12 +77,14 @@ export type Signal = { /** The function that we invoke for effects and computeds */ init: null | (() => V) | (() => void | (() => void)) | ((b: Block) => void | (() => void)); /** Anything that a signal owns */ - references: null | Signal[]; + references: null | ComputationSignal[]; /** The latest value for this signal, doubles as the teardown for effects */ value: V; }; -export type EffectSignal = Signal void)>; +export type Signal = SourceSignal | ComputationSignal; + +export type EffectSignal = ComputationSignal void)>; export type MaybeSignal = T | Signal; @@ -92,7 +107,7 @@ export type BlockType = export type TemplateNode = Text | Element | Comment; export type Transition = { - effect: Signal; + effect: EffectSignal; payload: null | TransitionPayload; init: (from?: DOMRect) => TransitionPayload; finished: (fn: () => void) => void; @@ -106,7 +121,7 @@ export type Transition = { export type RootBlock = { dom: null | TemplateNode | Array; - effect: null | Signal; + effect: null | ComputationSignal; container: Node; intro: boolean; parent: null; @@ -117,7 +132,7 @@ export type RootBlock = { export type IfBlock = { current: boolean; dom: null | TemplateNode | Array; - effect: null | Signal; + effect: null | ComputationSignal; parent: Block; transition: null | ((transition: Transition) => void); type: typeof IF_BLOCK; @@ -125,7 +140,7 @@ export type IfBlock = { export type KeyBlock = { dom: null | TemplateNode | Array; - effect: null | Signal; + effect: null | ComputationSignal; parent: Block; transition: null | ((transition: Transition) => void); type: typeof KEY_BLOCK; @@ -133,7 +148,7 @@ export type KeyBlock = { export type HeadBlock = { dom: null | TemplateNode | Array; - effect: null | Signal; + effect: null | ComputationSignal; parent: Block; transition: null | ((transition: Transition) => void); type: typeof HEAD_BLOCK; @@ -141,7 +156,7 @@ export type HeadBlock = { export type DynamicElementBlock = { dom: null | TemplateNode | Array; - effect: null | Signal; + effect: null | ComputationSignal; parent: Block; transition: null | ((transition: Transition) => void); type: typeof DYNAMIC_ELEMENT_BLOCK; @@ -149,7 +164,7 @@ export type DynamicElementBlock = { export type DynamicComponentBlock = { dom: null | TemplateNode | Array; - effect: null | Signal; + effect: null | ComputationSignal; parent: Block; transition: null | ((transition: Transition) => void); type: typeof DYNAMIC_COMPONENT_BLOCK; @@ -157,7 +172,7 @@ export type DynamicComponentBlock = { export type AwaitBlock = { dom: null | TemplateNode | Array; - effect: null | Signal; + effect: null | ComputationSignal; parent: Block; pending: boolean; transition: null | ((transition: Transition) => void); @@ -169,7 +184,7 @@ export type EachBlock = { flags: number; dom: null | TemplateNode | Array; items: EachItemBlock[]; - effect: null | Signal; + effect: null | ComputationSignal; parent: Block; transition: null | ((transition: Transition) => void); transitions: Array; @@ -178,7 +193,7 @@ export type EachBlock = { export type EachItemBlock = { dom: null | TemplateNode | Array; - effect: null | Signal; + effect: null | ComputationSignal; item: any | Signal; index: number | Signal; key: unknown; @@ -191,7 +206,7 @@ export type EachItemBlock = { export type SnippetBlock = { dom: null | TemplateNode | Array; parent: Block; - effect: null | Signal; + effect: null | ComputationSignal; transition: null; type: typeof SNIPPET_BLOCK; };