diff --git a/.changeset/hungry-dots-fry.md b/.changeset/hungry-dots-fry.md new file mode 100644 index 000000000..5c2328f6f --- /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/compiler/phases/3-transform/client/visitors/global.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js index 4c1d8af7b..4b053e056 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js @@ -14,21 +14,21 @@ export const global_visitors = { }, MemberExpression(node, { state, next }) { if (node.object.type === 'ThisExpression') { - // rewrite `this.#foo` as `this.#foo.value` inside a constructor + // rewrite `this.#foo` as `this.#foo.v` inside a constructor if (node.property.type === 'PrivateIdentifier') { const field = state.private_state.get(node.property.name); if (field) { - return state.in_constructor ? b.member(node, b.id('value')) : b.call('$.get', node); + return state.in_constructor ? b.member(node, b.id('v')) : b.call('$.get', node); } } - // rewrite `this.foo` as `this.#foo.value` inside a constructor + // rewrite `this.foo` as `this.#foo.v` inside a constructor if (node.property.type === 'Identifier' && !node.computed) { const field = state.public_state.get(node.property.name); if (field && state.in_constructor) { - return b.member(b.member(b.this, field.id), b.id('value')); + return b.member(b.member(b.this, field.id), b.id('v')); } } } diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index c74105372..b860a4c3f 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -26,7 +26,6 @@ import { EACH_IS_CONTROLLED, EACH_INDEX_REACTIVE, EACH_ITEM_REACTIVE, - EACH_IS_ANIMATED, PassiveDelegatedEvents, DelegatedEvents } from '../../constants.js'; @@ -624,7 +623,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(() => { @@ -2083,7 +2082,7 @@ export function update_each_item_block(block, item, index, type) { if (transitions !== null && (type & EACH_KEYED) !== 0) { let prev_index = block.index; if (index_is_reactive) { - prev_index = /** @type {import('./types.js').Signal} */ (prev_index).value; + prev_index = /** @type {import('./types.js').Signal} */ (prev_index).v; } const items = block.parent.items; if (prev_index !== index && /** @type {number} */ (index) < items.length) { @@ -2125,7 +2124,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)); } } @@ -2244,11 +2243,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re ? [] : Array.from(maybe_array); if (key_fn !== null) { - const length = array.length; - keys = Array(length); - for (let i = 0; i < length; i++) { - keys[i] = key_fn(array[i]); - } + keys = array.map(key_fn); } if (fallback_fn !== null) { if (array.length === 0) { @@ -3163,7 +3158,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 7e3173663..3800b9021 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1,7 +1,6 @@ import { subscribe_to_store } from '../../store/utils.js'; import { EMPTY_FUNC } from '../common.js'; import { unwrap } from './render.js'; -import { map_delete, map_get, map_set } from './operations.js'; import { is_array } from './utils.js'; export const SOURCE = 1; @@ -46,7 +45,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,37 +132,54 @@ 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 { + c: null, + // We can remove this if we get rid of beforeUpdate/afterUpdate + x: null, + e: null, + f: flags, + v: 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, - context: null, - dependencies: null, - destroy: null, - equals: null, - flags, - init: null, - references: null, - value + b: block, + c: null, + x: null, + d: null, + y: null, + e: null, + f: flags, + i: null, + r: null, + v: value }; } /** - * @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) { - const references = target_signal.references; + const references = target_signal.r; if (references === null) { - target_signal.references = [ref_signal]; + target_signal.r = [ref_signal]; } else { references.push(ref_signal); } @@ -175,29 +191,31 @@ function push_reference(target_signal, ref_signal) { * @returns {boolean} */ function is_signal_dirty(signal) { - const flags = signal.flags; - if ((flags & DIRTY) !== 0 || signal.value === UNINITIALIZED) { + const flags = signal.f; + if ((flags & DIRTY) !== 0 || signal.v === UNINITIALIZED) { return true; } if ((flags & MAYBE_DIRTY) !== 0) { - const dependencies = signal.dependencies; + const dependencies = /** @type {import('./types.js').ComputationSignal} **/ (signal).d; if (dependencies !== null) { const length = dependencies.length; let i; for (i = 0; i < length; i++) { const dependency = dependencies[i]; - const dep_flags = dependency.flags; - if ((dep_flags & MAYBE_DIRTY) !== 0 && !is_signal_dirty(dependency)) { + if ((dependency.f & MAYBE_DIRTY) !== 0 && !is_signal_dirty(dependency)) { set_signal_status(dependency, CLEAN); continue; } // 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); + if ((dependency.f & DIRTY) !== 0) { + if ((dependency.f & DERIVED) !== 0) { + update_derived( + /** @type {import('./types.js').ComputationSignal} **/ (dependency), + true + ); // Might have been mutated from above get. - if ((signal.flags & DIRTY) !== 0) { + if ((signal.f & DIRTY) !== 0) { return true; } } else { @@ -212,25 +230,25 @@ 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) { - const init = signal.init; + const init = signal.i; const previous_dependencies = current_dependencies; const previous_dependencies_index = current_dependencies_index; const previous_consumer = current_consumer; const previous_block = current_block; const previous_component_context = current_component_context; const previous_skip_consumer = current_skip_consumer; - const is_render_effect = (signal.flags & RENDER_EFFECT) !== 0; + const is_render_effect = (signal.f & RENDER_EFFECT) !== 0; const previous_untracking = current_untracking; current_dependencies = /** @type {null | import('./types.js').Signal[]} */ (null); current_dependencies_index = 0; current_consumer = signal; - current_block = signal.block; - current_component_context = signal.context; - current_skip_consumer = current_effect === null && (signal.flags & UNOWNED) !== 0; + current_block = signal.b; + current_component_context = signal.x; + current_skip_consumer = current_effect === null && (signal.f & UNOWNED) !== 0; current_untracking = false; // Render effects are invoked when the UI is about to be updated - run beforeUpdate at that point @@ -242,12 +260,12 @@ function execute_signal_fn(signal) { let res; if (is_render_effect) { res = /** @type {(block: import('./types.js').Block) => V} */ (init)( - /** @type {import('./types.js').Block} */ (signal.block) + /** @type {import('./types.js').Block} */ (signal.b) ); } else { res = /** @type {() => V} */ (init)(); } - let dependencies = signal.dependencies; + let dependencies = /** @type {import('./types.js').Signal[]} **/ (signal.d); if (current_dependencies !== null) { let i; @@ -259,17 +277,19 @@ function execute_signal_fn(signal) { dependencies[current_dependencies_index + i] = current_dependencies[i]; } } else { - signal.dependencies = dependencies = current_dependencies; + signal.d = /** @type {import('./types.js').Signal[]} **/ ( + dependencies = current_dependencies + ); } if (!current_skip_consumer) { for (i = current_dependencies_index; i < dependencies.length; i++) { const dependency = dependencies[i]; - if (dependency.consumers === null) { - dependency.consumers = [signal]; + if (dependency.c === null) { + dependency.c = [signal]; } else { - dependency.consumers.push(signal); + dependency.c.push(signal); } } } @@ -291,23 +311,23 @@ 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} */ function remove_consumer(signal, start_index, remove_unowned) { - const dependencies = signal.dependencies; + const dependencies = signal.d; if (dependencies !== null) { let i; for (i = start_index; i < dependencies.length; i++) { const dependency = dependencies[i]; - const consumers = dependency.consumers; + const consumers = dependency.c; let consumers_length = 0; if (consumers !== null) { consumers_length = consumers.length - 1; if (consumers_length === 0) { - dependency.consumers = null; + dependency.c = null; } else { const index = consumers.indexOf(signal); // Swap with last element and then remove. @@ -315,8 +335,12 @@ function remove_consumer(signal, start_index, remove_unowned) { consumers.pop(); } } - if (remove_unowned && consumers_length === 0 && (dependency.flags & UNOWNED) !== 0) { - remove_consumer(dependency, 0, true); + if (remove_unowned && consumers_length === 0 && (dependency.f & UNOWNED) !== 0) { + remove_consumer( + /** @type {import('./types.js').ComputationSignal} **/ (dependency), + 0, + true + ); } } } @@ -324,21 +348,21 @@ 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) { - const references = signal.references; - signal.references = null; + const references = signal.r; + signal.r = null; if (references !== null) { let i; for (i = 0; i < references.length; i++) { const reference = references[i]; - if ((reference.flags & IS_EFFECT) !== 0) { + if ((reference.f & IS_EFFECT) !== 0) { destroy_signal(reference); } else { remove_consumer(reference, 0, true); - reference.dependencies = null; + reference.d = null; } } } @@ -363,10 +387,10 @@ function report_error(block, error) { * @returns {void} */ export function execute_effect(signal) { - if ((signal.flags & DESTROYED) !== 0) { + if ((signal.f & DESTROYED) !== 0) { return; } - const teardown = signal.value; + const teardown = signal.v; const previous_effect = current_effect; current_effect = signal; @@ -377,10 +401,10 @@ export function execute_effect(signal) { } const possible_teardown = execute_signal_fn(signal); if (typeof possible_teardown === 'function') { - signal.value = possible_teardown; + signal.v = possible_teardown; } } catch (error) { - const block = signal.block; + const block = signal.b; if (block !== null) { report_error(block, error); } else { @@ -389,10 +413,10 @@ export function execute_effect(signal) { } finally { current_effect = previous_effect; } - const component_context = signal.context; + const component_context = signal.x; if ( is_runes(component_context) && // Don't rerun pre effects more than once to accomodate for "$: only runs once" behavior - (signal.flags & PRE_EFFECT) !== 0 && + (signal.f & PRE_EFFECT) !== 0 && current_queued_pre_and_render_effects.length > 0 ) { flush_local_pre_effects(component_context); @@ -416,7 +440,7 @@ function flush_queued_effects(effects) { let i; for (i = 0; i < length; i++) { const signal = effects[i]; - const flags = signal.flags; + const flags = signal.f; if ((flags & (DESTROYED | INERT)) === 0) { if (is_signal_dirty(signal)) { set_signal_status(signal, CLEAN); @@ -452,7 +476,7 @@ function process_microtask() { * @returns {void} */ export function schedule_effect(signal, sync) { - const flags = signal.flags; + const flags = signal.f; if (sync || (flags & SYNC_EFFECT) !== 0) { execute_effect(signal); set_signal_status(signal, CLEAN); @@ -499,7 +523,7 @@ export function flush_local_render_effects() { const effects = []; for (let i = 0; i < current_queued_pre_and_render_effects.length; i++) { const effect = current_queued_pre_and_render_effects[i]; - if ((effect.flags & RENDER_EFFECT) !== 0 && effect.context === current_component_context) { + if ((effect.f & RENDER_EFFECT) !== 0 && effect.x === current_component_context) { effects.push(effect); current_queued_pre_and_render_effects.splice(i, 1); i--; @@ -516,7 +540,7 @@ export function flush_local_pre_effects(context) { const effects = []; for (let i = 0; i < current_queued_pre_and_render_effects.length; i++) { const effect = current_queued_pre_and_render_effects[i]; - if ((effect.flags & PRE_EFFECT) !== 0 && effect.context === context) { + if ((effect.f & PRE_EFFECT) !== 0 && effect.x === context) { effects.push(effect); current_queued_pre_and_render_effects.splice(i, 1); i--; @@ -575,20 +599,20 @@ export async function tick() { /** * @template V - * @param {import('./types.js').Signal} signal + * @param {import('./types.js').ComputationSignal} signal * @param {boolean} force_schedule * @returns {void} */ function update_derived(signal, force_schedule) { const value = execute_signal_fn(signal); const status = - current_skip_consumer || (current_effect === null && (signal.flags & UNOWNED) !== 0) + current_skip_consumer || (current_effect === null && (signal.f & UNOWNED) !== 0) ? DIRTY : CLEAN; set_signal_status(signal, status); - const equals = /** @type {import('./types.js').EqualsFunctions} */ (signal.equals); - if (!equals(value, signal.value)) { - signal.value = value; + const equals = /** @type {import('./types.js').EqualsFunctions} */ (signal.e); + if (!equals(value, signal.v)) { + signal.v = value; mark_signal_consumers(signal, DIRTY, force_schedule); } } @@ -615,10 +639,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 +701,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); } }); } @@ -704,9 +730,9 @@ export function exposable(fn) { * @returns {V} */ export function get(signal) { - const flags = signal.flags; + const flags = signal.f; if ((flags & DESTROYED) !== 0) { - return signal.value; + return signal.v; } if (is_signal_exposed && current_should_capture_signal) { @@ -718,13 +744,9 @@ export function get(signal) { } // Register the dependency on the current consumer signal. - if ( - current_consumer !== null && - (current_consumer.flags & MANAGED) === 0 && - !current_untracking - ) { - const unowned = (current_consumer.flags & UNOWNED) !== 0; - const dependencies = current_consumer.dependencies; + if (current_consumer !== null && (current_consumer.f & MANAGED) === 0 && !current_untracking) { + const unowned = (current_consumer.f & UNOWNED) !== 0; + const dependencies = current_consumer.d; if ( current_dependencies === null && dependencies !== null && @@ -734,15 +756,15 @@ export function get(signal) { current_dependencies_index++; } else if (current_dependencies === null) { current_dependencies = [signal]; - } else if (signal !== current_dependencies.at(-1)) { + } else if (signal !== current_dependencies[current_dependencies.length - 1]) { current_dependencies.push(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; + return signal.v; } /** @@ -845,19 +867,20 @@ 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} */ export function mark_subtree_inert(signal, inert) { - const flags = signal.flags; - if (((flags & INERT) === 0 && inert) || ((flags & INERT) !== 0 && !inert)) { - signal.flags ^= INERT; + const flags = signal.f; + const is_already_inert = (flags & INERT) !== 0; + if (is_already_inert !== inert) { + signal.f ^= INERT; if (!inert && (flags & IS_EFFECT) !== 0 && (flags & CLEAN) === 0) { schedule_effect(/** @type {import('./types.js').EffectSignal} */ (signal), false); } } - const references = signal.references; + const references = signal.r; if (references !== null) { let i; for (i = 0; i < references.length; i++) { @@ -874,14 +897,14 @@ export function mark_subtree_inert(signal, inert) { * @returns {void} */ function mark_signal_consumers(signal, to_status, force_schedule) { - const runes = is_runes(signal.context); - const consumers = signal.consumers; + const runes = is_runes(signal.x); + const consumers = signal.c; if (consumers !== null) { const length = consumers.length; let i; for (i = 0; i < length; i++) { const consumer = consumers[i]; - const flags = consumer.flags; + const flags = consumer.f; const unowned = (flags & UNOWNED) !== 0; const dirty = (flags & DIRTY) !== 0; // We skip any effects that are already dirty (but not unowned). Additionally, we also @@ -895,7 +918,7 @@ function mark_signal_consumers(signal, to_status, force_schedule) { // are already dirty. Unowned signals might be dirty because they are not captured as part of an // effect. if ((flags & CLEAN) !== 0 || (dirty && unowned)) { - if ((consumer.flags & IS_EFFECT) !== 0) { + if ((consumer.f & IS_EFFECT) !== 0) { schedule_effect(/** @type {import('./types.js').EffectSignal} */ (consumer), false); } else { mark_signal_consumers(consumer, MAYBE_DIRTY, force_schedule); @@ -916,8 +939,8 @@ export function set_signal_value(signal, value) { !current_untracking && !ignore_mutation_validation && current_consumer !== null && - is_runes(signal.context) && - (current_consumer.flags & DERIVED) !== 0 + is_runes(signal.x) && + (current_consumer.f & DERIVED) !== 0 ) { throw new Error( "Unsafe mutations during Svelte's render or derived phase are not permitted in runes mode. " + @@ -926,11 +949,11 @@ export function set_signal_value(signal, value) { ); } if ( - (signal.flags & SOURCE) !== 0 && - !(/** @type {import('./types.js').EqualsFunctions} */ (signal.equals)(value, signal.value)) + (signal.f & SOURCE) !== 0 && + !(/** @type {import('./types.js').EqualsFunctions} */ (signal.e)(value, signal.v)) ) { - const component_context = signal.context; - signal.value = value; + const component_context = signal.x; + signal.v = value; // If the current signal is running for the first time, it won't have any // consumers as we only allocate and assign the consumers after the signal // has fully executed. So in the case of ensuring it registers the consumer @@ -942,8 +965,8 @@ export function set_signal_value(signal, value) { if ( is_runes(component_context) && current_effect !== null && - current_effect.consumers === null && - (current_effect.flags & CLEAN) !== 0 && + current_effect.c === null && + (current_effect.f & CLEAN) !== 0 && current_dependencies !== null && current_dependencies.includes(signal) ) { @@ -969,22 +992,23 @@ 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 teardown = /** @type {null | (() => void)} */ (signal.v); + const destroy = signal.y; + const flags = signal.f; destroy_references(signal); remove_consumer(signal, 0, true); - signal.init = null; - signal.references = null; - signal.destroy = null; - signal.context = null; - signal.block = null; - signal.value = /** @type {V} */ (null); - signal.dependencies = null; - signal.consumers = null; + signal.i = null; + signal.r = null; + signal.y = null; + signal.x = null; + signal.b = null; + signal.v = /** @type {V} */ (null); + signal.d = null; + signal.c = null; set_signal_status(signal, DESTROYED); if (destroy !== null) { if (is_array(destroy)) { @@ -996,7 +1020,7 @@ export function destroy_signal(signal) { destroy(); } } - if (teardown !== null && (signal.flags & IS_EFFECT) !== 0) { + if (teardown !== null && (flags & IS_EFFECT) !== 0) { teardown(); } } @@ -1005,18 +1029,18 @@ 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; - signal.equals = get_equals_method(equals); + signal.i = init; + signal.x = current_component_context; + signal.e = get_equals_method(equals); if (!is_unowned) { push_reference(/** @type {import('./types.js').EffectSignal} */ (current_effect), signal); } @@ -1027,13 +1051,13 @@ 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); - source.context = current_component_context; - source.equals = get_equals_method(equals); + const source = create_source_signal(SOURCE | CLEAN, initial_value); + source.x = current_component_context; + source.e = get_equals_method(equals); return source; } @@ -1079,9 +1103,9 @@ 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); - signal.init = init; - signal.context = current_component_context; + const signal = create_computation_signal(type | DIRTY, null, block); + signal.i = init; + signal.x = current_component_context; if (schedule) { schedule_effect(signal, sync); } @@ -1100,7 +1124,7 @@ export function user_effect(init) { throw new Error('The Svelte $effect rune can only be used during component initialisation.'); } const apply_component_effect_heuristics = - current_effect.flags & RENDER_EFFECT && + current_effect.f & RENDER_EFFECT && current_component_context !== null && !current_component_context.mounted; const effect = internal_create_effect( @@ -1158,7 +1182,7 @@ export function pre_effect(init) { 'The Svelte $effect.pre rune can only be used during component initialisation.' ); } - const sync = current_effect !== null && (current_effect.flags & RENDER_EFFECT) !== 0; + const sync = current_effect !== null && (current_effect.f & RENDER_EFFECT) !== 0; return internal_create_effect( PRE_EFFECT, () => { @@ -1210,18 +1234,18 @@ 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} */ export function push_destroy_fn(signal, destroy_fn) { - let destroy = signal.destroy; + let destroy = signal.y; if (destroy === null) { - signal.destroy = destroy_fn; + signal.y = destroy_fn; } else if (is_array(destroy)) { destroy.push(destroy_fn); } else { - signal.destroy = [destroy, destroy_fn]; + signal.y = [destroy, destroy_fn]; } } @@ -1232,16 +1256,16 @@ export function push_destroy_fn(signal, destroy_fn) { * @returns {void} */ export function set_signal_status(signal, status) { - const flags = signal.flags; + const flags = signal.f; if ((flags & status) === 0) { if ((flags & MAYBE_DIRTY) !== 0) { - signal.flags ^= MAYBE_DIRTY; + signal.f ^= MAYBE_DIRTY; } else if ((flags & CLEAN) !== 0) { - signal.flags ^= CLEAN; + signal.f ^= CLEAN; } else if ((flags & DIRTY) !== 0) { - signal.flags ^= DIRTY; + signal.f ^= DIRTY; } - signal.flags ^= status; + signal.f ^= status; } } @@ -1254,7 +1278,7 @@ export function is_signal(val) { return ( typeof val === 'object' && val !== null && - typeof (/** @type {import('./types.js').Signal} */ (val).flags) === 'number' + typeof (/** @type {import('./types.js').Signal} */ (val).f) === 'number' ); } @@ -1300,9 +1324,9 @@ export function prop_source(props_obj, key, default_value, call_default_value) { if ( is_signal(possible_signal) && - possible_signal.value === value && + possible_signal.v === value && update_bound_prop === undefined && - get_equals_method() === possible_signal.equals + get_equals_method() === possible_signal.e ) { if (should_set_default_value) { set( @@ -1344,7 +1368,7 @@ export function prop_source(props_obj, key, default_value, call_default_value) { return; } - if (not_equal(immutable, propagating_value, source_signal.value)) { + if (not_equal(immutable, propagating_value, source_signal.v)) { ignore_next2 = true; // TODO figure out why we need it this way and the explain in a comment; // some tests fail is we just do set_signal_value(source_signal, propagating_value) @@ -1366,7 +1390,7 @@ export function prop_source(props_obj, key, default_value, call_default_value) { return; } - if (not_equal(immutable, propagating_value, possible_signal.value)) { + if (not_equal(immutable, propagating_value, possible_signal.v)) { ignore_next1 = true; untrack(() => update_bound_prop(propagating_value)); } diff --git a/packages/svelte/src/internal/client/transitions.js b/packages/svelte/src/internal/client/transitions.js index b07db9335..357e287d4 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 5bc4ce730..1b67b0eb7 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -46,30 +46,52 @@ export type ComponentContext = { }; }; -export type Signal = { - /** The block associated with this effect/computed */ - block: null | Block; - /** Signals that read from the current signal */ - consumers: null | Signal[]; - /** The associated component if this signal is an effect/computed */ - context: null | ComponentContext; - /** Signals that this signal reads from */ - dependencies: null | Signal[]; - /** Thing(s) that need destroying */ - destroy: null | (() => void) | Array<() => void>; - /** For value equality */ - equals: null | EqualsFunctions; +// For both SourceSignal and ComputationSignal, we use signal character property string. +// This now only reduces code-size and parsing, but it also improves the performance of the JIT compiler. +// It's likely not to have any real wins wwhen the JIT is disabled. Lastly, we keep two shapes rather than +// a single monomorphic shape to improve the memory usage. Source signals don't need the same shape as they +// simply don't do as much as computations (effects and derived signals). Thus we can improve the memory +// profile at the slight cost of some runtime performance. + +export type SourceSignal = { + /** consumers: Signals that read from the current signal */ + c: null | ComputationSignal[]; + /** context: The associated component if this signal is an effect/computed */ + x: null | ComponentContext; + /** equals: For value equality */ + e: null | EqualsFunctions; + /** flags: The types that the signal represent, as a bitwise value */ + f: SignalFlags; + /** value: The latest value for this signal */ + v: V; +}; + +export type ComputationSignal = { + /** block: The block associated with this effect/computed */ + b: null | Block; + /** consumers: Signals that read from the current signal */ + c: null | ComputationSignal[]; + /** context: The associated component if this signal is an effect/computed */ + x: null | ComponentContext; + /** dependencies: Signals that this signal reads from */ + d: null | Signal[]; + /** destroy: Thing(s) that need destroying */ + y: null | (() => void) | Array<() => void>; + /** equals: For value equality */ + e: null | EqualsFunctions; /** The types that the signal represent, as a bitwise value */ - flags: SignalFlags; - /** 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[]; - /** The latest value for this signal, doubles as the teardown for effects */ - value: V; + f: SignalFlags; + /** init: The function that we invoke for effects and computeds */ + i: null | (() => V) | (() => void | (() => void)) | ((b: Block) => void | (() => void)); + /** references: Anything that a signal owns */ + r: null | ComputationSignal[]; + /** value: The latest value for this signal, doubles as the teardown for effects */ + v: V; }; -export type EffectSignal = Signal void)>; +export type Signal = SourceSignal | ComputationSignal; + +export type EffectSignal = ComputationSignal void)>; export type MaybeSignal = T | Signal; @@ -92,7 +114,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 +128,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 +139,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 +147,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 +155,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 +163,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 +171,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 +179,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 +191,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 +200,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 +213,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; }; diff --git a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js index 25dcd16ae..836fd0ed0 100644 --- a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js @@ -20,10 +20,10 @@ export default function Class_state_field_constructor_assignment($$anchor, $$pro #b = $.source(); constructor() { - this.#a.value = 1; - this.#b.value = 2; + this.#a.v = 1; + this.#b.v = 2; } } $.pop(); -} \ No newline at end of file +}