From b6e04e69e7cd244255463309a9dadb561d0bef90 Mon Sep 17 00:00:00 2001 From: Milo Mighdoll <14153763+milomg@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:00:13 -0500 Subject: [PATCH 1/3] Experiment with different source/observer tracking mechanism (#2576) * begin using new graph * Add changeset --------- Co-authored-by: Ryan Carniato --- .changeset/poor-shirts-design.md | 5 + packages/solid/src/reactive/signal.ts | 188 ++++++++++++++++------- packages/solid/test/signals.memo.spec.ts | 2 +- 3 files changed, 138 insertions(+), 57 deletions(-) create mode 100644 .changeset/poor-shirts-design.md diff --git a/.changeset/poor-shirts-design.md b/.changeset/poor-shirts-design.md new file mode 100644 index 000000000..2b3f01f0c --- /dev/null +++ b/.changeset/poor-shirts-design.md @@ -0,0 +1,5 @@ +--- +"solid-js": minor +--- + +Provide an improved source/observer tracking mechanism diff --git a/packages/solid/src/reactive/signal.ts b/packages/solid/src/reactive/signal.ts index ddce76160..ae348afc7 100644 --- a/packages/solid/src/reactive/signal.ts +++ b/packages/solid/src/reactive/signal.ts @@ -84,8 +84,8 @@ export interface SourceMapValue { export interface SignalState extends SourceMapValue { value: T; - observers: Computation[] | null; - observerSlots: number[] | null; + observers: ReactiveLink | null; + observersTail: ReactiveLink | null; tValue?: T; comparator?: (prev: T, next: T) => boolean; // development-only @@ -101,15 +101,25 @@ export interface Owner { name?: string; } +interface ReactiveLink { + source: SignalState | Memo; + observer: Computation; + nextSource: ReactiveLink | null; + prevObserver: ReactiveLink | null; + nextObserver: ReactiveLink | null; + version: number; +} + export interface Computation extends Owner { fn: EffectFunction; state: ComputationState; tState?: ComputationState; - sources: SignalState[] | null; - sourceSlots: number[] | null; + sources: ReactiveLink | null; + sourcesTail: ReactiveLink | null; value?: Init; updatedAt: number | null; pure: boolean; + relinkTime: number; user?: boolean; suspense?: SuspenseContextType; } @@ -169,7 +179,7 @@ export function createRoot(fn: RootFunction, detachedOwner?: typeof Owner) throw new Error("Dispose method must be an explicit argument to createRoot function"); }) : fn - : () => fn(() => untrack(() => cleanNode(root))); + : () => fn(() => untrack(() => cleanupRoot(root))); if (IS_DEV) DevHooks.afterCreateOwner && DevHooks.afterCreateOwner(root); @@ -235,7 +245,7 @@ export function createSignal( const s: SignalState = { value, observers: null, - observerSlots: null, + observersTail: null, comparator: options.equals || undefined }; @@ -455,7 +465,7 @@ export function createMemo( ) as Partial>; c.observers = null; - c.observerSlots = null; + c.observersTail = null; c.comparator = options.equals || undefined; if (Scheduler && Transition && Transition.running) { c.tState = STALE; @@ -1146,7 +1156,7 @@ export function devComponent(Comp: (props: P) => V, props: P): V { ) as DevComponent

; c.props = props; c.observers = null; - c.observerSlots = null; + c.observersTail = null; c.name = Comp.name; c.component = Comp; updateComputation(c); @@ -1306,26 +1316,55 @@ export function readSignal(this: SignalState | Memo) { } } if (Listener) { - const sSlot = this.observers ? this.observers.length : 0; - if (!Listener.sources) { - Listener.sources = [this]; - Listener.sourceSlots = [sSlot]; - } else { - Listener.sources.push(this); - Listener.sourceSlots!.push(sSlot); - } - if (!this.observers) { - this.observers = [Listener]; - this.observerSlots = [Listener.sources.length - 1]; - } else { - this.observers.push(Listener); - this.observerSlots!.push(Listener.sources.length - 1); - } + link(this, Listener); } if (runningTransition && Transition!.sources.has(this)) return this.tValue; return this.value; } +function link(source: SignalState | Memo, observer: Computation) { + const prevSource = observer.sourcesTail; + if (prevSource !== null && prevSource.source === source) { + return; + } + const nextSource = prevSource !== null ? prevSource.nextSource : observer.sources; + const time = observer.relinkTime; + if (nextSource !== null && nextSource.source === source) { + nextSource.version = time; + observer.sourcesTail = nextSource; + return; + } + const prevObserver = source.observersTail; + if ( + prevObserver !== null && + prevObserver.version === observer.relinkTime && + prevObserver.observer === observer + ) { + return; + } + const newLink = + (observer.sourcesTail = + source.observersTail = + { + source: source, + observer: observer, + nextSource: nextSource, + prevObserver: prevObserver, + nextObserver: null, + version: time + }); + if (prevSource !== null) { + prevSource.nextSource = newLink; + } else { + observer.sources = newLink; + } + if (prevObserver !== null) { + prevObserver.nextObserver = newLink; + } else { + source.observers = newLink; + } +} + export function writeSignal(node: SignalState | Memo, value: any, isComp?: boolean) { let current = Transition && Transition.running && Transition.sources.has(node) ? node.tValue : node.value; @@ -1338,10 +1377,12 @@ export function writeSignal(node: SignalState | Memo, value: any, isCo } if (!TransitionRunning) node.value = value; } else node.value = value; - if (node.observers && node.observers.length) { + if (node.observers && node.observers) { runUpdates(() => { - for (let i = 0; i < node.observers!.length; i += 1) { - const o = node.observers![i]; + for (let link = node.observers; link !== null; link = link.nextObserver) { + const o = link.observer; + if (link.version < o.relinkTime) continue; + const TransitionRunning = Transition && Transition.running; if (TransitionRunning && Transition!.disposed.has(o)) continue; if (TransitionRunning ? !o.tState : !o.state) { @@ -1365,7 +1406,9 @@ export function writeSignal(node: SignalState | Memo, value: any, isCo function updateComputation(node: Computation) { if (!node.fn) return; - cleanNode(node); + node.sourcesTail = null; + node.relinkTime = ExecCount; + cleanupRoot(node); const time = ExecCount; runComputation( node, @@ -1374,6 +1417,7 @@ function updateComputation(node: Computation) { : node.value, time ); + afterComputation(node); if (Transition && !Transition.running && Transition.sources.has(node as Memo)) { queueMicrotask(() => { @@ -1398,11 +1442,11 @@ function runComputation(node: Computation, value: any, time: number) { if (node.pure) { if (Transition && Transition.running) { node.tState = STALE; - (node as Memo).tOwned && (node as Memo).tOwned!.forEach(cleanNode); + (node as Memo).tOwned && (node as Memo).tOwned!.forEach(destroyNode); (node as Memo).tOwned = undefined; } else { node.state = STALE; - node.owned && node.owned.forEach(cleanNode); + node.owned && node.owned.forEach(destroyNode); node.owned = null; } } @@ -1437,10 +1481,11 @@ function createComputation( updatedAt: null, owned: null, sources: null, - sourceSlots: null, + sourcesTail: null, cleanups: null, value: init, owner: Owner, + relinkTime: 0, context: Owner ? Owner.context : null, pure }; @@ -1557,12 +1602,12 @@ function completeUpdates(wait: boolean) { } Transition = null; runUpdates(() => { - for (const d of disposed) cleanNode(d); + for (const d of disposed) destroyNode(d); for (const v of sources) { v.value = v.tValue; if ((v as Memo).owned) { for (let i = 0, len = (v as Memo).owned!.length; i < len; i++) - cleanNode((v as Memo).owned![i]); + destroyNode((v as Memo).owned![i]); } if ((v as Memo).tOwned) (v as Memo).owned = (v as Memo).tOwned!; delete v.tValue; @@ -1636,8 +1681,8 @@ function lookUpstream(node: Computation, ignore?: Computation) { const runningTransition = Transition && Transition.running; if (runningTransition) node.tState = 0; else node.state = 0; - for (let i = 0; i < node.sources!.length; i += 1) { - const source = node.sources![i] as Memo; + for (let link = node.sources; link !== null; link = link.nextSource) { + const source = link.source as Memo; if (source.sources) { const state = runningTransition ? source.tState : source.state; if (state === STALE) { @@ -1650,8 +1695,9 @@ function lookUpstream(node: Computation, ignore?: Computation) { function markDownstream(node: Memo) { const runningTransition = Transition && Transition.running; - for (let i = 0; i < node.observers!.length; i += 1) { - const o = node.observers![i]; + for (let link = node.observers; link !== null; link = link.nextObserver) { + const o = link.observer; + if (link.version < o.relinkTime) continue; if (runningTransition ? !o.tState : !o.state) { if (runningTransition) o.tState = PENDING; else o.state = PENDING; @@ -1662,39 +1708,24 @@ function markDownstream(node: Memo) { } } -function cleanNode(node: Owner) { +function cleanupRoot(node: Owner) { let i; - if ((node as Computation).sources) { - while ((node as Computation).sources!.length) { - const source = (node as Computation).sources!.pop()!, - index = (node as Computation).sourceSlots!.pop()!, - obs = source.observers; - if (obs && obs.length) { - const n = obs.pop()!, - s = source.observerSlots!.pop()!; - if (index < obs.length) { - n.sourceSlots![s] = index; - obs[index] = n; - source.observerSlots![index] = s; - } - } - } - } if ((node as Memo).tOwned) { for (i = (node as Memo).tOwned!.length - 1; i >= 0; i--) - cleanNode((node as Memo).tOwned![i]); + destroyNode((node as Memo).tOwned![i]); delete (node as Memo).tOwned; } if (Transition && Transition.running && (node as Memo).pure) { reset(node as Computation, true); } else if (node.owned) { - for (i = node.owned.length - 1; i >= 0; i--) cleanNode(node.owned[i]); + for (i = node.owned.length - 1; i >= 0; i--) destroyNode(node.owned[i]); + node.owned = null; } if (node.cleanups) { - for (i = node.cleanups.length - 1; i >= 0; i--) node.cleanups[i](); + for (let i = node.cleanups.length - 1; i >= 0; i--) node.cleanups[i](); node.cleanups = null; } if (Transition && Transition.running) (node as Computation).tState = 0; @@ -1702,6 +1733,50 @@ function cleanNode(node: Owner) { IS_DEV && delete node.sourceMap; } +function afterComputation(node: Computation) { + const sourcesTail = node.sourcesTail as ReactiveLink | null; + let toRemove = sourcesTail !== null ? sourcesTail.nextSource : node.sources; + if (toRemove !== null) { + do { + toRemove = unlinkFromObservers(toRemove); + } while (toRemove !== null); + if (sourcesTail !== null) { + sourcesTail.nextSource = null; + } else { + node.sources = null; + } + } +} + +function unlinkFromObservers(link: ReactiveLink): ReactiveLink | null { + const source = link.source; + const nextSource = link.nextSource; + const nextObserver = link.nextObserver; + const prevObserver = link.prevObserver; + if (nextObserver !== null) { + nextObserver.prevObserver = prevObserver; + } else { + source.observersTail = prevObserver; + } + if (prevObserver !== null) { + prevObserver.nextObserver = nextObserver; + } else { + source.observers = nextObserver; + } + return nextSource; +} + +function destroyNode(node: Computation) { + let link = node.sources; + while (link) { + link = unlinkFromObservers(link); + } + node.sources = null; + node.sourcesTail = null; + + cleanupRoot(node); +} + function reset(node: Computation, top?: boolean) { if (!top) { node.tState = 0; @@ -1735,6 +1810,7 @@ function handleError(err: unknown, owner = Owner) { fn() { runErrors(error, fns, owner); }, + sources: null, state: STALE } as unknown as Computation); else runErrors(error, fns, owner); diff --git a/packages/solid/test/signals.memo.spec.ts b/packages/solid/test/signals.memo.spec.ts index 7fd911f34..34e4626a7 100644 --- a/packages/solid/test/signals.memo.spec.ts +++ b/packages/solid/test/signals.memo.spec.ts @@ -362,7 +362,7 @@ describe("createMemo", () => { expect(order).toBe("t1t2c1c2"); order = ""; set(3); - expect(order).toBe("t2c2t1c1"); + expect(order).toBe("t1c2t2c1"); }); }); From 374f77b9777eadb06697770cd5a97007b49db0d1 Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Mon, 26 Jan 2026 09:04:12 -0800 Subject: [PATCH 2/3] enter prerelease --- .changeset/pre.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 000000000..8279bcc5b --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,12 @@ +{ + "mode": "pre", + "tag": "beta", + "initialVersions": { + "babel-preset-solid": "1.9.10", + "solid-js": "1.9.11", + "solid-element": "1.9.1", + "solid-ssr": "1.7.2", + "test-integration": "1.9.11" + }, + "changesets": [] +} From 24a08ede554c1779c02ba4cd123b889001f4007c Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Mon, 26 Jan 2026 09:09:00 -0800 Subject: [PATCH 3/3] v1.10.0-beta.0 --- .changeset/pre.json | 4 +++- packages/babel-preset-solid/CHANGELOG.md | 7 +++++++ packages/babel-preset-solid/package.json | 4 ++-- packages/solid-element/CHANGELOG.md | 7 +++++++ packages/solid-element/package.json | 4 ++-- packages/solid/CHANGELOG.md | 6 ++++++ packages/solid/package.json | 2 +- packages/test-integration/CHANGELOG.md | 8 ++++++++ packages/test-integration/package.json | 2 +- 9 files changed, 37 insertions(+), 7 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 8279bcc5b..a0bee80fb 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -8,5 +8,7 @@ "solid-ssr": "1.7.2", "test-integration": "1.9.11" }, - "changesets": [] + "changesets": [ + "poor-shirts-design" + ] } diff --git a/packages/babel-preset-solid/CHANGELOG.md b/packages/babel-preset-solid/CHANGELOG.md index 82a24144c..8f46807c5 100644 --- a/packages/babel-preset-solid/CHANGELOG.md +++ b/packages/babel-preset-solid/CHANGELOG.md @@ -1,5 +1,12 @@ # babel-preset-solid +## 1.10.0-beta.0 + +### Patch Changes + +- Updated dependencies [b6e04e6] + - solid-js@1.10.0-beta.0 + ## 1.9.10 ### Patch Changes diff --git a/packages/babel-preset-solid/package.json b/packages/babel-preset-solid/package.json index 370387c7c..1f102ba3e 100644 --- a/packages/babel-preset-solid/package.json +++ b/packages/babel-preset-solid/package.json @@ -1,6 +1,6 @@ { "name": "babel-preset-solid", - "version": "1.9.10", + "version": "1.10.0-beta.0", "description": "Babel preset to transform JSX for Solid.js", "author": "Ryan Carniato ", "homepage": "https://github.com/solidjs/solid/blob/main/packages/babel-preset-solid#readme", @@ -18,7 +18,7 @@ }, "peerDependencies": { "@babel/core": "^7.0.0", - "solid-js": "^1.9.10" + "solid-js": "^1.10.0-beta.0" }, "peerDependenciesMeta": { "solid-js": { diff --git a/packages/solid-element/CHANGELOG.md b/packages/solid-element/CHANGELOG.md index 37a48eae2..34000bbd9 100644 --- a/packages/solid-element/CHANGELOG.md +++ b/packages/solid-element/CHANGELOG.md @@ -1,5 +1,12 @@ # solid-element +## 1.10.0-beta.0 + +### Patch Changes + +- Updated dependencies [b6e04e6] + - solid-js@1.10.0-beta.0 + ## 1.9.1 ### Patch Changes diff --git a/packages/solid-element/package.json b/packages/solid-element/package.json index 1e7b411e3..9448b7c80 100644 --- a/packages/solid-element/package.json +++ b/packages/solid-element/package.json @@ -3,7 +3,7 @@ "description": "Webcomponents wrapper for Solid", "author": "Ryan Carniato", "license": "MIT", - "version": "1.9.1", + "version": "1.10.0-beta.0", "homepage": "https://github.com/solidjs/solid/blob/main/packages/solid-element#readme", "type": "module", "main": "dist/index.js", @@ -21,7 +21,7 @@ "component-register": "^0.8.7" }, "peerDependencies": { - "solid-js": "^1.9.11" + "solid-js": "^1.10.0-beta.0" }, "devDependencies": { "solid-js": "workspace:*" diff --git a/packages/solid/CHANGELOG.md b/packages/solid/CHANGELOG.md index b7937d2a9..232d2f0f7 100644 --- a/packages/solid/CHANGELOG.md +++ b/packages/solid/CHANGELOG.md @@ -1,5 +1,11 @@ # solid-js +## 1.10.0-beta.0 + +### Minor Changes + +- b6e04e6: Provide an improved source/observer tracking mechanism + ## 1.9.11 ### Patch Changes diff --git a/packages/solid/package.json b/packages/solid/package.json index 3f6c63b7f..e52ac35c5 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -1,7 +1,7 @@ { "name": "solid-js", "description": "A declarative JavaScript library for building user interfaces.", - "version": "1.9.11", + "version": "1.10.0-beta.0", "author": "Ryan Carniato", "license": "MIT", "homepage": "https://solidjs.com", diff --git a/packages/test-integration/CHANGELOG.md b/packages/test-integration/CHANGELOG.md index e86b5850e..7dfbf476e 100644 --- a/packages/test-integration/CHANGELOG.md +++ b/packages/test-integration/CHANGELOG.md @@ -1,5 +1,13 @@ # test-integration +## 1.10.0-beta.0 + +### Patch Changes + +- Updated dependencies [b6e04e6] + - solid-js@1.10.0-beta.0 + - babel-preset-solid@1.10.0-beta.0 + ## 1.9.11 ### Patch Changes diff --git a/packages/test-integration/package.json b/packages/test-integration/package.json index e48c96159..5b2ad0e33 100644 --- a/packages/test-integration/package.json +++ b/packages/test-integration/package.json @@ -14,5 +14,5 @@ "gitly": "^2.2.1", "shelljs": "^0.8.5" }, - "version": "1.9.11" + "version": "1.10.0-beta.0" }