From 890c443493b4d29d3da25ddd3f3d0daa5adceec3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 09:41:35 +0000 Subject: [PATCH 1/3] perf(solid,signals): trim the bytes #4279 and #4281 added laneRead (#4281): the D-28 verdict-lane exit shares the never-shown test with the A15 arm it falls into, computed once inside one EFFECT_RENDER block; `!guess && status & STATUS_PENDING` is hoisted to one local, and the untracked arm falls through to the shared enterLane tail instead of repeating it. Same evaluation order and side effects. hydratedCreateErrorBoundary (#4279): the content's no-claim window runs through hydrateWindow (no id, no scope, hydration already on, snapshot root already marked: it only swaps the claim owner) while hydrating, and plainly after (claims are read only while hydrating); the has/load checks and the two plain-boundary returns merge into one fall-through. Local, minified, this commit alone: page: live server components -73 B (signals -35, solid -38); app: hydrating -38 B; signals: + isPending/latest -35 B. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1 --- packages/signals/src/core/lanes.ts | 59 +++++++++++--------------- packages/solid/src/client/hydration.ts | 32 ++++++-------- 2 files changed, 39 insertions(+), 52 deletions(-) diff --git a/packages/signals/src/core/lanes.ts b/packages/signals/src/core/lanes.ts index f70e0c2563..22ec77baa7 100644 --- a/packages/signals/src/core/lanes.ts +++ b/packages/signals/src/core/lanes.ts @@ -568,6 +568,7 @@ export function laneRead(c: Computed | null, el: Signal | Computed | null, el: Signal | Computed( fn: () => T, fallback: (error: () => unknown, reset: () => void) => U ): Accessor { - if (!sharedConfig.hydrating || noHydrationId()) return coreErrorBoundary(fn, fallback); - markTopLevelSnapshotScope(); - const parent = getOwner()!; - const expectedId = peekNextChildId(parent); - if (sharedConfig.has!(expectedId)) { - const err = sharedConfig.load!(expectedId); + if (sharedConfig.hydrating && !noHydrationId()) { + markTopLevelSnapshotScope(); + const expectedId = peekNextChildId(getOwner()!); + let err: unknown; // The server drew the fallback; the client's own run decides. The // content runs now with its data adopted by id but claiming no DOM — // the markup is the fallback's — so what it throws is the client's @@ -1285,21 +1283,19 @@ function hydratedCreateErrorBoundary( // the server's fallback up behind a hold: a render effect in the content // throwing the server's error until reset(), disposed with the content // when it re-runs (#4273). - if (err !== undefined) { + if (sharedConfig.has!(expectedId) && (err = sharedConfig.load!(expectedId)) !== undefined) { // The content's owner, until a fallback is built. let o: any; const read = coreErrorBoundary( - () => { - const prev = _claimOwner; - o = getOwner(); - // A claim window over no owner: nothing under it claims. - _claimOwner = {} as Owner; - try { - return fn(); - } finally { - _claimOwner = prev; - } - }, + // While hydrating, a claim window over no owner: nothing under it + // claims. (`hydrateWindow` with no id or scope, hydration already on + // and the snapshot root marked above, only swaps the claim owner.) + // A later run claims nothing either way: claims are read only while + // hydrating. + () => + _hydratingValue + ? hydrateWindow("", fn, undefined, ((o = getOwner()), {} as Owner)) + : fn(), (e, reset) => ((o = 0), fallback(e, reset)) ); // No fallback yet: the content ran clean. Hold the server's fallback From 8314d650654ca77b96fe470a676905e79710f122 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 09:41:36 +0000 Subject: [PATCH 2/3] perf(signals): fold hook-only arms away in builds without the hook module A call site guarded only by state the hook module sets (`fam.opt`, a boundary's reveal slot or controller, a lane's marks, CONFIG_GUESS) kept its dead arm in a build that never imports that module: the bundler folds the never-assigned hook binding to `null` but cannot prove the guard false, so `null.view(...)`-style calls shipped. Each site now checks the hook first (truthy, as async.ts already does), so the arm folds: - store optimistic arms (optHooks: store.ts, reconcile.ts, affects.ts) - reveal-order arms in boundaries (revealHooks), the boundary flip's laneStageHook call - recompute's laneOutcomeHook arm, land()'s releaseMarksHook call The hooks are installed once at module evaluation and never cleared, and the guarded state is only ever set by those modules, so behaviour is unchanged wherever the hook is installed (one extra truthy check). Also: the production halt message is one shared constant, and the plain flush (no lane seams) no longer swaps in a queue pair only a lane seam writes to. Local (rolldown), base -> head, minified / brotli: app: compiled floor (hello world) 27,475 -> 27,403 (-72) / 9,995 -> 9,953 (-42) app: render + one signal 26,944 -> 26,871 (-73) / 9,796 -> 9,768 (-28) signals: + createStore 44,488 -> 43,881 (-607) / 15,059 -> 14,892 (-167) app: compiled CSR 80,478 -> 79,641 (-837) / 26,299 -> 26,075 (-224) app: CSR with Show/For/... 37,183 -> 36,886 (-297) / 13,396 -> 13,321 (-75) (totals with the previous commit) Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1 --- packages/signals/src/boundaries.ts | 22 ++++++++++++--------- packages/signals/src/core/core.ts | 4 +++- packages/signals/src/core/scheduler.ts | 11 +++++++---- packages/signals/src/store/affects.ts | 2 +- packages/signals/src/store/hooks.ts | 3 +++ packages/signals/src/store/reconcile.ts | 6 +++--- packages/signals/src/store/store.ts | 26 ++++++++++++------------- 7 files changed, 43 insertions(+), 31 deletions(-) diff --git a/packages/signals/src/boundaries.ts b/packages/signals/src/boundaries.ts index c8103258ea..7b36a765cb 100644 --- a/packages/signals/src/boundaries.ts +++ b/packages/signals/src/boundaries.ts @@ -148,6 +148,9 @@ export interface RevealHooks { _unregister(controller: object, b: Boundary): void; _changed(controller: object): void; } +// Every call site checks the hooks themselves first (not only the slot or +// controller, which are never set without them): a build that does not +// import `createRevealOrder` folds the reveal-order arms away. export let revealHooks: RevealHooks | null = null; export function setRevealHooks(h: RevealHooks): void { revealHooks = h; @@ -193,7 +196,7 @@ function caught(b: Boundary, node: Computed, error: unknown): void { // nothing on screen. The fallback is instantiated once per show — a later // error reaches it through the `error` accessor, which is why it is one. if (!b._fallback) redraw(b); - if (b._reveal !== null) revealHooks!._changed(b._reveal); + if (revealHooks && b._reveal !== null) revealHooks._changed(b._reveal); } /** A re-armed boundary found something pending: fresh again, and the swap @@ -206,7 +209,8 @@ function flip(b: Boundary): void { b._lane = b._armLane; const o = b._output!; if (!b._lane && o._config & CONFIG_HELD && txOf(o) !== flushTransaction) unhold(o); - if (b._lane !== null && b._output !== null) laneStageHook!(b._output, b._lane, true, false); + if (laneStageHook && b._lane !== null && b._output !== null) + laneStageHook(b._output, b._lane, true, false); } export function redraw(b: Boundary): void { @@ -240,7 +244,7 @@ function catchStatus( b._readers.size === 0 ) { if (b._fallback) redraw(b); - if (b._reveal !== null) revealHooks!._changed(b._reveal); + if (revealHooks && b._reveal !== null) revealHooks._changed(b._reveal); } return false; } @@ -384,7 +388,7 @@ function boundarySeam(): void { if (b._readers.size === 0) { collecting.delete(b); if (b._fallback) redraw(b); - if (b._reveal !== null) revealHooks!._changed(b._reveal); + if (revealHooks && b._reveal !== null) revealHooks._changed(b._reveal); } } } @@ -558,7 +562,7 @@ function createBoundary( if (getOwner()) cleanup(() => { collecting.delete(b); - if (controller) revealHooks!._unregister(controller, b); + if (revealHooks && controller) revealHooks._unregister(controller, b); }); // The `on` dependencies live OUTSIDE the boundary, as the condition of a // `` wrapping it would: created before the owner's context names it. @@ -598,7 +602,7 @@ function createBoundary( b._tree = tree; // A slot of the reveal order in its context (reveal.ts): gated until the // controller releases it — before the output's first pass. - if (controller) revealHooks!._register(controller, b); + if (revealHooks && controller) revealHooks._register(controller, b); const output = computed( (): T => { // Reveal order (reveal.ts): a gated slot shows its fallback — or, in a @@ -612,7 +616,7 @@ function createBoundary( // settled): the pass goes on to show it — the redraw it asks for is // refused mid-pass and not needed. link(tree, getOwner() as Computed); - if (b._reveal !== null) revealHooks!._changed(b._reveal); + if (revealHooks && b._reveal !== null) revealHooks._changed(b._reveal); if (b._gated) { if (b._collapsed) { b._fallback = true; @@ -717,10 +721,10 @@ function createBoundary( release(b); // Shown: the slot leaves its reveal order (a later re-arm gates nobody // else; the frontier moves on). - if (b._reveal !== null) { + if (revealHooks && b._reveal !== null) { const controller = b._reveal; b._reveal = null; - revealHooks!._changed(controller); + revealHooks._changed(controller); } return value; }, diff --git a/packages/signals/src/core/core.ts b/packages/signals/src/core/core.ts index fc84981e29..7f0b970edc 100644 --- a/packages/signals/src/core/core.ts +++ b/packages/signals/src/core/core.ts @@ -706,11 +706,13 @@ export function recompute(el: Computed, create: boolean = false): void { // confirm is a landing too — the lane's end commits it) and notifies // only a correction: an equal truth re-runs nothing (lanes.ts). (As the // lane's own work the lane judged it — `laneStage`.) + // (The hook first: a build without lanes folds the arm away.) if ( + laneOutcomeHook && !create && lane === null && el._config & CONFIG_GUESS && - laneOutcomeHook!(el, value, errored) + laneOutcomeHook(el, value, errored) ) { // The truth, staged under the lane's parent. } else if (errored) { diff --git a/packages/signals/src/core/scheduler.ts b/packages/signals/src/core/scheduler.ts index 6ab9b4fc08..2ea0f4561c 100644 --- a/packages/signals/src/core/scheduler.ts +++ b/packages/signals/src/core/scheduler.ts @@ -726,12 +726,14 @@ export function schedule() { */ export const ROOT_ERROR_HOOK: unique symbol = Symbol.for("solid-js/root-error-hook") as any; +const HALTED = "[REACTIVITY_HALTED]"; + export function haltReactivity(cause?: unknown): void { if (halted) return; halted = true; // A cleanup error collected before the halt is dropped (owner.ts). rethrowCleanupError(true); - let message = "[REACTIVITY_HALTED]"; + let message = HALTED; if (__DEV__) { message += " An uncaught error halted the reactive system. No further updates will be processed. Handle errors with or treat this as a crash."; @@ -785,7 +787,7 @@ function notifyHalted(): void { __DEV__ ? "[REACTIVITY_HALTED] Update ignored: the reactive system was halted by an earlier uncaught error." + haltedAt - : "[REACTIVITY_HALTED]" + : HALTED ); } @@ -1016,11 +1018,12 @@ export class GlobalQueue implements IQueue { // This flush's own runs, the lanes' reveals, and the landings' held runs // are collected apart and ordered below. const own = this._queues; - const lanes: [QueueCallback[], QueueCallback[]] = (this._queues = [[], []]); + const lanes: [QueueCallback[], QueueCallback[]] = [[], []]; // The passes that read this frame's stagings as the screen read held // writes if it parked: the lane seam re-derives them on the committed // world (a lane's runs wait this round). if (laneSeamsHook) { + this._queues = lanes; laneSeamsHook(t !== null ? stagedReaders : null); if (stagedReaders.length) stagedReaders.length = 0; } @@ -1179,7 +1182,7 @@ function land(u: Transaction): void { // (`_nodes` still lists them). if (__OBSERVE__ && attrHooks !== null) attrHooks.transitionSettled(u); const frame = reruns(u); - if (u._marks !== null) releaseMarksHook!(u._marks); + if (releaseMarksHook && u._marks !== null) releaseMarksHook(u._marks); // Old children die in the commits (cleanups first), then the stashed // effects run ahead of this flush's own. A node a lane took over since // (a guess written over the staged truth) is the lane's to land. diff --git a/packages/signals/src/store/affects.ts b/packages/signals/src/store/affects.ts index 3c701470df..7ec3246d0b 100644 --- a/packages/signals/src/store/affects.ts +++ b/packages/signals/src/store/affects.ts @@ -87,7 +87,7 @@ function walk( if (t !== undefined) { if (t.pb !== null) entry._scope.add(t.v); // The writer's view: the tick's own unflushed guesses are in motion too. - if (t.fam?.opt === true) raw = optHooks!.view(t, raw, true); + if (optHooks && t.fam?.opt === true) raw = optHooks.view(t, raw, true); const nodes = t.n; if (nodes !== null) for (const k of Reflect.ownKeys(nodes)) diff --git a/packages/signals/src/store/hooks.ts b/packages/signals/src/store/hooks.ts index b3b141bdc0..a0b7a2a66e 100644 --- a/packages/signals/src/store/hooks.ts +++ b/packages/signals/src/store/hooks.ts @@ -35,6 +35,9 @@ export interface OptHooks { * into the draft. */ reconcile(draft: any, incoming: any, keyFn: ((item: any) => any) | null): void; } +// Set before any family is optimistic (`fam.opt`): a call site checks the +// hooks first, so a build that does not import optimistic.ts folds the arm +// away. export let optHooks: OptHooks | null = null; export function installOptHooks(hooks: OptHooks): void { optHooks = hooks; diff --git a/packages/signals/src/store/reconcile.ts b/packages/signals/src/store/reconcile.ts index 3b031aaf09..2b4a664853 100644 --- a/packages/signals/src/store/reconcile.ts +++ b/packages/signals/src/store/reconcile.ts @@ -57,8 +57,8 @@ export function reconcileState( // truth but an optimistic edit — the keyed diff is written into the draft // (a matched row keeps its proxy, its changed leaves become its guesses), // and the setter's exit turns the draft into guesses like any. - if (userWriting() && t.fam?.opt === true) { - optHooks!.reconcile(state, unwrapValue(value), keyFn); + if (optHooks && userWriting() && t.fam?.opt === true) { + optHooks.reconcile(state, unwrapValue(value), keyFn); return; } // Replace-mode root handed another store's proxy: chain to it (§7b). @@ -100,7 +100,7 @@ function applyAdopt(t: StoreTarget, incoming: any, keyFn: KeyFn | null, proj = f // Q-D: a landing on an optimistic family is reconciled against the VIEW // — key matching from the arrangement the guesses made (R28), so a row // the guess added keeps its proxy when the server returns it. - const prevView = t.fam?.opt === true ? optHooks!.view(t, old) : old; + const prevView = optHooks && t.fam?.opt === true ? optHooks.view(t, old) : old; const nextArr = Array.isArray(incoming); const shallow = t.s === true; adoptPB(t, incoming, false); diff --git a/packages/signals/src/store/store.ts b/packages/signals/src/store/store.ts index 52d67a5bcb..0abec66c32 100644 --- a/packages/signals/src/store/store.ts +++ b/packages/signals/src/store/store.ts @@ -690,8 +690,8 @@ function ensurePB(target: StoreTarget): Record { // An optimistic edit composes on the view the user saw and becomes // guesses at the setter's exit: nothing is staged, nothing folds // (optimistic.ts). - if (userWrite && target.fam?.opt === true && !pendingNotify.has(target)) - return (target.pb = optHooks!.draft(target)); + if (optHooks && userWrite && target.fam?.opt === true && !pendingNotify.has(target)) + return (target.pb = optHooks.draft(target)); if (pb !== null && !isOwned(pb)) { // An adopted object awaiting its fold is the user's: the draft writes a // clone of it. The clone is the batch's staging — unless the adoption is @@ -1142,12 +1142,12 @@ function notifyWrites(t: StoreTarget): void { // Optimistic channel: a user's writes on an optimistic family become // guesses on the nodes (lanes); the draft is discarded, the committed // backing never touched. - if (userWrite && t.fam?.opt === true) { + if (optHooks && userWrite && t.fam?.opt === true) { // The draft is discarded first: the diff reads the target (its nodes // are born from the committed frame, not the draft). t.pb = null; t.wk = null; - t.pb = optHooks!.writes(t, pb); + t.pb = optHooks.writes(t, pb); return; } const old = t.v; @@ -1888,10 +1888,10 @@ function enumerationSource(target: StoreTarget): Record { // A container's arrangement guess is not a backing: the view composes // over the base it is shown over. const base = asBacking(readNode(getContainerNode(target))); - return target.fam?.opt === true ? optHooks!.view(target, base) : base; + return optHooks && target.fam?.opt === true ? optHooks.view(target, base) : base; } const src = readSource(target); - return optRead(target) ? optHooks!.view(target, src) : src; + return optHooks && optRead(target) ? optHooks.view(target, src) : src; } /** A read on an optimistic family that composes the lanes' values: not the @@ -2165,8 +2165,8 @@ const traps: ProxyHandler = { node._value = present; // the link follows the inner store (S4) if (node._config & CONFIG_OVERRIDE) return !!nv; return present; - } else if (optRead(target)) { - const guessed = optHooks!.has(target, key); + } else if (optHooks && optRead(target)) { + const guessed = optHooks.has(target, key); if (guessed !== undefined) return guessed; } } @@ -2207,8 +2207,8 @@ const traps: ProxyHandler = { return undefined; } else src = readSource(target, key, true); let desc: PropertyDescriptor | undefined; - if (!enumerating && optRead(target)) { - const guessed = optHooks!.descriptor(target, key); + if (optHooks && !enumerating && optRead(target)) { + const guessed = optHooks.descriptor(target, key); if (guessed === null) return undefined; desc = guessed; } @@ -2412,8 +2412,8 @@ export function storeSetter(proxy: T, fn: (draft: T) => T | void, guard = tru // A returned replacement: on an optimistic family a user's is itself an // optimistic edit — diffed against the view as guesses (`next` parity); // otherwise it is adopted as the incoming truth. - if (guard && target.fam?.opt === true && !writeOverride) - optHooks!.writes(target, unwrapValue(result)); + if (optHooks && guard && target.fam?.opt === true && !writeOverride) + optHooks.writes(target, unwrapValue(result)); else adoptPB(target, unwrapValue(result)); } // A34 (3), #3612 — the derived store's twin of `setMemo`: a user write to @@ -2614,7 +2614,7 @@ function snapshotWalk(value: any, seen: Map, fam: StoreFamily | nul if (t.ovl) materializePB(t); backing = t.pb!; } - if (optRead(t)) backing = optHooks!.view(t, backing); + if (optHooks && optRead(t)) backing = optHooks.view(t, backing); if (backing === src) break; src = backing; } From 63b57d6ac4dbf1fdd3c0e9690561defb598b7a72 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 09:47:35 +0000 Subject: [PATCH 3/3] chore: changeset for the byte trim Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1 --- .changeset/trim-batch-bytes-1026.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/trim-batch-bytes-1026.md diff --git a/.changeset/trim-batch-bytes-1026.md b/.changeset/trim-batch-bytes-1026.md new file mode 100644 index 0000000000..45f9bfe0be --- /dev/null +++ b/.changeset/trim-batch-bytes-1026.md @@ -0,0 +1,6 @@ +--- +"@solidjs/signals": patch +"solid-js": patch +--- + +Smaller bundles: builds without the optimistic-store, reveal-order or lane hook modules drop their dead branches, and the #4273/#4275 fixes share existing paths.