github solidjs/solid solid-js@2.0.0-rc.10

Patch Changes

  • fd36d37: attribution.enable(opts) now returns the release of the hold it takes (idempotent, like subscribe), and options combine across holds by the most demanding request per key (the log prints while any holder wants it, a check runs while any holder wants it at the most sensitive threshold asked for, historyLimit is the largest), so a hold adds to what the engine does and never takes away what another asked for — a track enabled with log: false beside a console session leaves its log alone, in either order — instead of every call rebuilding the options from defaults. disable() is the full teardown whatever holds are outstanding (the console's reset), so re-enabling to reopen a window and calling disable() once cannot strand a hold. enablePerformanceTracks releases through the token. Dev builds create a component's console.createTask task only for components rendered while an attribution engine is installed — the stack capture per call roughly doubled dev mount for a session with nothing enabled; the tracks enabled at bootstrap still see every component's site.

  • fd36d37: Attribution engine: timeline records, and a flushStart hook

    Five new listener-gated records on attribution.subscribe: create (a computation's creation run — the mount flame), effect (an effect callback, timed and joined to its compute run), flush (one scheduler drain: runs, creations, whether it parked a transition, the interaction it served), flight (an async flight from origin to landing or abandonment, with the async node's owner path) and fallback (a loading boundary's fallback from show to hide). None is built, logged or folded unless something is subscribed to its type, so the console/agent readers pay nothing for records only a timeline wants. OBSERVE.subjectOf answers for the node-bearing ones.

    Core: a flushStart hook beside flushEnd (one drain, never nested), and effectRunStart now fires in observe builds like its effectRunEnd twin, so writes inside effect callbacks carry their effect origin in observe too, not only in dev. The observe core grows by 67 bytes minified; prod is byte-identical.

    The engine's effect-frame → node map is now filled by the first write inside a callback rather than by every callback (every reader resolves it through a write's origin, and most effect callbacks never write), which removes a WeakMap write per effect callback from the enabled engine's hot path.

    @solidjs/web/performance-tracks paints the new records: creation runs and effect callbacks on the Effects/Memos tracks, drains on the Propagation track (one wave per drain), flights and fallbacks on a new Async track.

  • 43fae6e: onCleanup callbacks on one owner now run in reverse registration order (unwind), restoring the 1.x #1562 semantics; in production, component bodies share the enclosing owner, so a parent that registers cleanup before rendering its children now tears down after them, matching dev (#3572).

  • 21b9784: A "recovery" record on OBSERVE.records (RecoveryEvent): the client rendering a <Loading> boundary the server handed over — its fragment rejected or the stream was cut — with how long the fallback stood (waitedMs) and what the fresh render cost (renderMs), joined to the server's "boundary" record by id.

  • 56918c6: Close a serialized async iterator once the response that carries it is abandoned. When a renderToStream consumer cancels the readable or a pipe sink throws, the render is disposed, but the tapped iterator handed to the serializer kept delegating next() to the source, so an iterable memo, an async memo that resolves to an iterable, and a generator projection kept pulling (or never ran their finally) for as long as the source lived. The tapped iterator now answers done and calls the source's return() once its computation is disposed, and the projection pump closes its source when it stops for disposal. The render's wind-down also closes the serializer, which returns every async iterator it is still pulling — including a source whose pending next() never settles, which the disposed check on the next pull could never reach.

  • fd36d37: Rename the compilers' componentNames option to sourceNames, now boolean | { components?: boolean }. sourceNames: true (or { components: true }) is what componentNames: true was — the tag as written in source as createComponent's third argument, on DOM and SSR output. The option is now the home for every kind of source name the compilers can carry into output for the dev and observe runtimes to label the reactive graph with (binding effects and primitives follow); the object form picks kinds. @solidjs/compiler rejects componentNames as an unknown option, so a stale @solidjs/vite-plugin fails loudly rather than compiling without labels.

  • c74365d: createRoot JSDoc now states that a root created inside an owner is disposed with it (detach with runWithOwner(null, …)); MIGRATION note added.

  • 7599885: Dev diagnostic UNSCOPED_HOLE_ALLOCATED_IDS: an unscoped hole that took hydration ids at a position the other side does not share (#3567 follow-up)

    The one hydration-key gap left after #3599 is a bare identifier bound to a function — const renderHead = () => props.header; <div>{renderHead}</div>. The compiler sees a value and scopes nothing; both runtimes unwrap the function, but not at the same point (the client's insert at the statement, the server's ssr() inside the walk after every scoped sibling reserved its slot), so the keys of the hole's content and of the holes after it permute. JSX.Element excludes functions in 2.0, so type-checked code cannot write this hole; by ruling it is not scoped (no production cost for a shape the types reject). Instead the dev builds detect the permutation and raise UNSCOPED_HOLE_ALLOCATED_IDS (warn, kind render, once per site) on both server render and client hydrate. The server reports structurally — the counter's next id when the hole was registered (data.registered) differs from the one it was evaluated at (data.before, data.after after it ran); the client, which always builds in place, reports when the content it built inside an unscoped function hole moved the counter and missed a server-rendered key. A function hole with nothing scoped after it lands on the same ids on both sides and stays silent (a boundary's zero-arity fallback={() => <F />} thunk built by the consuming hole is that shape). data.name is the function; the message names the fix: call the function at the hole ({renderHead()}) or pass the built value. Scoped holes, memo and component accessors, children(), <For> rows and the runtime's own children inserts never raise it.

    Plumbing: a dev-only sharedConfig.devPeekNextContextId() on both solid-js facades (the next child id of the current owner, read without consuming — on the server without materializing a pending hole slot), installed under the dev gate so the prod and observe artifacts of solid-js and @solidjs/web are byte-identical to before.

  • 6717d35: Diagnostic code consolidation. Codes are public API (the Sentry fingerprint roots); three renames.

    • WIDE_WRITE is folded into HUGE_FAN_OUT — one code, one threshold story. The core's always-on check warns at 2000 subscribers; the attribution engine's fanOut threshold (default 250, was wideWrites) warns earlier while it is enabled and reports through the same emitter, so data.count is the subscriber count and, from the engine, data.write says which write reached it ("write", "refresh", "async"). One WeakMap dedupes both reporters (re-warn after another 500). AttributionOptions.wideWrites → fanOut.
    • SERVER_FN_ERROR_SANITIZED and SSR_ERROR_SANITIZED are one code, SERVER_ERROR_SANITIZED — the same fact from two roads. data.source is "server-function" (severity error, from @solidjs/web/server-functions) or "ssr" (severity info, from the SSR <Errored>/rejection path); data.error is the original and data.wire the replacement on both.
    • ASYNC_WATERFALL is client-only again: the server's boundary-passes verdict is its own code, SSR_BOUNDARY_WATERFALL (kind ssr; info for two sequential waits, warn for three or more; data: { boundary, passes, sequentialMs }). data.side is gone with it.
  • 75ed5e9: <Errored> builds a function-valued fallback inside its own scope, whatever its arity — like <Show> resolving a function child. A zero-arity thunk (fallback={() => <F />}, type-reachable since () => X is assignable to (err, reset) => X) used to be handed back unresolved for the consuming hole to build on the enclosing owner's counter, which permuted hydration keys whenever a scoped hole followed the boundary in the same element (surfaced by #3620). Zero-arity and two-arity fallbacks now allocate identically on server and client; reset, error narrowing, fallback reactivity and the dev console.error for a fallback that cannot see the error are unchanged. A rest-parameter fallback ((...args) => …, length 0) now receives err and reset too.

  • a360ad0: Fix SSR hanging when a derived async computation reads a bare ssrSource: "client" hole inside <Loading>. createMemo(async …), createProjection, and dynamic({ deferStream: true }) whose compute throws the client-hole NotReady now classify FINAL — the boundary hands the subtree off to the client exactly as a direct read does — instead of subscribing a retry to a source that never settles and leaving the response open. A client hole that surfaces before the shell has flushed now takes the same fallback + $$f route as one found at discovery, rather than rejecting the fragment over an empty region.

  • 2d64742: Fix a per-request SSR memory leak for bare ssrSource: "client" sources (#3657). A derived read of a client-only source (<Show when={client().length}>, a dynamic() source memo, an <Errored> aggregate) subscribed its retry to the shared never-settling client-hole promise; those subscriptions could never fire but were never released either, pinning each request's computation, props and data for the life of the process. The client hole is now an inert thenable rather than a native promise — then drops its callbacks — so no subscription site can accumulate anything on it. Rendered output is unchanged.

  • 3af4696: fix(web): an async dynamic() instance serializes its landing and the client adopts it (#3666)

    dynamic() no longer opts its memo out of hydration serialization. The per-instance value memo is an ordinary async memo: when the source introduced async (returned a thenable during SSR) its landing is serialized under the instance's id and the client memo adopts the record during hydration instead of re-running the source and waiting on it — the pending beat that committed the enclosing <Loading> to a fallback the server never rendered, then missed the SSR'd nodes. A server component lands as a flight reference (_$SC.r(id, address) now resolves to the call's binding, so a wrapped query() or async arrow around a server reference hydrates the same as a direct call), a tag name as its string, and a sync source lands nothing — unchanged.

    A source that resolves to a client component function cannot serialize and is now refused on the server at the point the memo would serialize it — new diagnostic DYNAMIC_ASYNC_COMPONENT (recorded on the observe channel; the memo rejects into the nearest <Errored> / onError in every tier). Move the async upstream (createAsync/createMemo read synchronously by the source) or use lazy().

    Wire shape: each async dynamic() instance now writes one hydration record, and every dynamic() instance allocates one more hydration owner id (keys under a dynamic() element shift by one slot).

  • a10d33b: Hybrid memo/signal handoff waits for the server answer to land and opens no pending window. An ssrSource: "hybrid" createMemo or function-form createSignal over an async generator re-ran the client generator at creation — a fresh flight ahead of the still-pending server answer, superseding it and reading pending from creation until the client's first yield — so a streamed <Loading> resuming to claim its fragment after the answer landed selected its fallback against resolved server content: a "Hydration key miss" warning and a flash of the fallback over the streamed content (the #3574 failure, on the value-shaped takeover). The handoff now follows the store's rules (#3551, #3593): it waits for the adopted answer to land, lands that answer as the handoff stream's synchronous first step, discards the client's duplicate first yield, adopts a rejected server answer, and is superseded by a dependency change before the landing. The node reads settled through the handoff, and isPending is false for it, until the client generator produces something new. Sync and promise-shaped hybrid computes are unchanged.

  • ae2bc9f: Hybrid store hydration waits for the server's answer before handing off to the client (#3498)

    Root cause: the hybrid store gate flipped synchronously at the end of the claim pass. With loadingValue/seedLoadingValue the server serializes a pending placeholder whose real answer arrives later over the stream; flipping before it landed let the client takeover supersede the server flight, so the engine dropped the server's answer and the store never showed it.

    The handoff now follows four rules:

    1. It waits for the first server answer to land. Synchronous when the serialized value is already settled, as before. When it is pending, the handoff happens when that answer lands — resolve or reject — by adopting it as a one-yield stream whose next pull (the engine's own continuation after the landing commits) is the handoff. It never waits on hydration end and never holds hydration open.
    2. Only the handoff run's first yield is the duplicate. Later runs (a dependency change, refresh()) run the client source against the real draft and commit their first yield normally; previously every run kept discarding it.
    3. A rejected server answer is the adopted answer. The store surfaces the error until a refresh() (a non-handoff run) replaces it; the client's handoff run does not paper over the rejection.
    4. A dependency change before the pending answer lands supersedes it. Like any new pending change, it cancels the incoming server answer: the store goes live on that run — genuinely new work, not a handoff, so its first yield commits — and the abandoned server flight's landing or rejection is dropped without applying to the store or running it again.
  • cf61b8e: Hybrid store handoff no longer opens a pending window (#3574). An ssrSource: "hybrid" store's client takeover run — the re-run that continues from the adopted server answer — made the store read pending until its duplicate first yield landed, so a streamed <Loading> resuming to claim its fragment after the answer landed selected its fallback against resolved server content: a "Hydration key miss" warning and a flash of the fallback over the streamed content. The handoff now lands the adopted answer as a synchronous first step (generator and promise-shaped sources alike); the store reads settled through the handoff, and isPending is false for it, until the client source produces something new.

  • 7742b28: Fix hydration adoption trace pulling an async iterable the compute did not construct. The trace-run (subFetch) primed async-generator computes by pulling their first step under mocked fetch/Promise; it now does so only when the returned value is its own iterator (a generator object), leaving live-call iterables and deserialized codec streams untouched. Pulling a foreign adapter minted its resolver through the mocked Promise, corrupting its queue — the next streamed value threw temp.s is not a function from the document's inline runtime. Surfaces with a nested-async live answer ({ progress: asyncIterable }) read by a child memo during hydration, and as #3647: a foreign iterable whose [Symbol.asyncIterator]() is the subscription itself (the router's liveQuery) was opened against a fetch that never settles, so it never connected after hydration.

  • 7742b28: Fix hydration of <Loading on={...}>: the server's boundary now accounts for the client's on dependency node when it fakes the boundary's nesting depth, so content and thunk-fallback element keys match. Previously every element under a boundary with on missed its key on hydration (detached duplicates, nothing interactive). The server Loading also passes on through to createLoadingBoundary.

  • ed60f05: A cleanup that disposes its own root (or throws) runs exactly once: the disposal list is detached before it runs (#3601).

  • f41c6a4: Fix renderToString exiting the process on a late async rejection (#3570). Every server async flight settles an internal deferred; under renderToString (no serialization channel, the sync <Loading> path never awaits the pending source), in a <NoHydration> zone, or for an unread source, nothing observed it, so an async memo/store/projection that rejected after the HTML was returned became an unhandledRejection. The deferred is now observed at creation; consumers see exactly what they saw before.

  • f41c6a4: Fix renderToStream hanging or throwing TypeError: Cannot read properties of undefined (reading 'emit') when an async read that rejects is the direct child of <Loading> (#3569).

    • solid-js: a bare child's throw (<Loading>{data()}</Loading>, or a component whose return is the read) now routes through the boundary's error handler exactly like a template hole's does — the fragment rejects and the client re-renders the subtree (handling: "client"), instead of escalating to a request failure pre-flush.
    • @solidjs/web: a render failure (failRender) now completes the consumer — the awaited promise resolves with the HTML produced so far, pipe() ends its sink, pipeTo()/readable close the writable — and the serializer's completion no longer assembles a shell on the disposed render.
  • fd8b3df: Fix hydrate() halting with TypeError: Cannot read properties of null (reading '_config') when a useHead({ tag: "link", props: { rel: "stylesheet", href } }) sheet is still loading as hydration reaches it (the default with an async entry script), including on a late streamed boundary resume. The waitAsset gate memo is created without an owner, and the hydrating createMemo tried to peek a hydration id from that null owner; the gate is now transparent, so hydration never sees it. useHead also no longer gates stylesheets while hydrating: that content is already visible, and a pending read inside a boundary's claim window made the boundary render fresh DOM beside the server's.

    A streamed <Loading> boundary whose fragment swap the server holds on a stylesheet ($dfs) now resumes when the swap lands, not when its _fr record settles. Resuming on the settle claimed against a document that did not have the content yet, then the delayed swap inserted a second copy.

  • 1d3ec5d: Server createProjection / createStore over an async iterable pump in frame scope like a memo does (Stage 8 B5): inside a server-owned frame render every yield lands in the store, commits the binding ledger (live holes reading the store re-emit) and holds the response until the source ends; reads follow the live state. A live-branded source there stays connected, and under a live component's document render a projection takes its first value and closes the source, so the document completes. The slot-border trace's snapshot now waits only for undrained writes, not for a pull in flight.

  • 1d3ec5d: Frame renders tear down when their reader is gone. serverComponentResponse's body cancel() and the request's abort (frameTransformResult / frameTransformFlightResult pass event.request.signal) now dispose the render through renderToStream's disconnect path — previously cancel() only dropped writes and the render ran on until its sources happened to end. renderToStream gains a signal?: AbortSignal option for this (the SSR_STREAM_ABANDONED finding reports it as data.reason: "signal"); renderToFrameStream, renderServerComponent and serverComponentResponse accept it through their options. A frame flight response stops at the frame in progress and skips the rest. In solid-js, the server pump over an async iterable in frame scope closes its source from the compute's disposal — return() now, not at the source's next yield — so a standing source (a change feed, a subscription) does not hold what it subscribed to until an event nobody would see.

  • 1d3ec5d: Live server components on the document face (Stage 8 B3)

    Server: a component a live server function answers with renders into the document under a live scope — every async source it reads takes its first value into the markup and is closed, so the document completes; nested server components inherit the scope. The scope is judged from the memo's owner, which also fixes an unbranded thenable-resolved stream in server-component scope serializing instead of pumping. New dev-only check SSR_UNDECLARED_LIVE_SOURCE: an undeclared async iterable still pumping five seconds into a document render is named (frame-stream renders are never judged).

    Client: the frames intercept is consulted synchronously by live() and its answer rides on the iterable (LIVE_LOCAL); a hydrating dynamic() adopts it as its value at t=0 — no request, no pending beat — and takes over at its hydration scope's release, re-yielding the adopted binding and connecting once at the live address. A boundary the page is still delivering is answered with a promise that lands at its reveal, so a dynamic(() => call()) over a streaming boundary waits for the document instead of fetching. dynamic's memo treats the same server-component instance (same component and address) as equal, so placeholder and per-address binding never remount.

  • 3218b7a: A held derivation is not a proposal (#3612). A mainline write to a writable memo (createSignal(fn)) or function-form store (createStore(fn)) whose staging is a pass result held by another transaction no longer suppresses that transaction's re-derivation: the write still joins the transaction (A34), and the derivation re-runs under the hold with the written value as prev. Same-frame "manual write wins" (#2692) and last-write-wins for writes made inside the transaction are unchanged.

  • b149cd2: - Hydration facades (createMemo, function-form createSignal/createOptimistic, function-form createStore/createOptimisticStore, createProjection, createErrorBoundary, createLoadingBoundary, effects) no longer throw when created with no owner (runWithOwner(null, …)) or under a root without an id while hydrating. A node with no id counter to consume has nothing to hydrate positionally, so it takes the same non-hydrating path as transparent: true. Function-form createSignal now honors transparent like createMemo does (#3609).

    • ssrSource: "hybrid" on a sync or promise-shaped compute is now identical to "server" for function-form stores, createOptimisticStore and createProjection, matching memos and function-form signals: the serialized value is adopted and the compute does not re-run on the client (no refetch) until a dependency changes or refresh(). The handoff — the client continuing the server's stream from the adopted answer — only arms when the compute returns an async iterable. The creation-time gate flip that memos and signals made for non-iterable shapes is gone too: its write was held by the hydration snapshot scope and replayed after hydration completed, which re-ran the compute live.
  • 7742b28: live claims the whole response and its post-hydration takeover fires per hydration scope.

    • A live connection now lives as long as its response: an answer holding nested promises or async iterables ({ meta, progress: gen }) keeps the connection open until every one has settled. A body ending with deferreds still open is a death — the loop reconnects and re-yields the whole answer, fresh; one whose deferreds all settled is a completion. Nothing is added to the wire: the decoder counts what the codec's own close records leave open (createJSONDeserializer's returned function gains an open() accessor beside abort()). Deferreds a reconnected-from death left open stay pending until the iteration ends for good, then fail.
    • In process, live() brands the answer that is the source — the top-level iterable, or a function-valued answer (a component) as a whole. Sources nested in a value answer are not branded: they are bounded and end on their own, and a document render pumps them to their end as it does any streamed answer (their sharing between the serializer and a reading memo is the SSR fix in the companion changeset).
    • The hydration takeover gate is keyed per snapshot scope: a live node in the shell reconnects when the root pass ends, a live node under a boundary when that boundary hydrates — no live node waits on another boundary or on page-wide hydration end. A later hydration pass (islands) arms its own gate.
    • The takeover run tells the live answer the value the page was served with (Symbol.for("solid.LiveResumeFrom")), which the loop names as its first connection's Last-Event-ID; a takeover that finds the same value on the server yields nothing.
  • c9e1954: Loading on follows the frame (#3540). When a dependency of on changes, the boundary still stops waiting on its current content immediately — the frame no longer waits for it — but its fallback swap now lands with the same frame as the change that caused it, instead of in the current frame beside content the change is still holding. Navigating product A → B inside an action (or by a write whose async is in flight) with the shell reading product(id) outside a <Loading on={id()}> that reads comments(id) goes [A] → [B + spinner] → [B + comments], not [A] → [A + spinner] → [B + spinner] → [B + comments]. If the comments land before the shell, no fallback is ever shown. Nothing else holding the frame, the fallback and the committed change land together in the same pass, as before.

    Read latest() in on (or any display-ahead state: isPending(), an optimistic signal) to keep the previous behavior — the fallback shows now, beside the still-held frame.

    If the same data the boundary is waiting on is also read outside it, the frame waits on that read and no fallback appears; DEV warns LOADING_ON_OUTSIDE_HOLD with the fix (move the outside read under the boundary). A frame held by the write's action or by other data past the content's landing shows no fallback either — a race, not a warning.

    Errored no longer accepts on (nor createErrorBoundary an on option). It was added in #3556 and never released in a stable — rc-only. Retry through the reset the fallback receives (fallback={(err, reset) => ...}), or re-mount the boundary on the dependency (<Show keyed when={id()}>).

  • 55779c0: Loading's on prop is a dependency list, not a key (#3540). The expression is tracked and its value is never compared: a write to anything it reads — plain, optimistic, or a source going pending — re-arms the boundary. A re-armed boundary that has something pending under it shows its fallback again; one with nothing pending does nothing (no fallback flash). latest() inside on is redundant.

    The re-arm lands in the current frame. A write that makes content pending is held by the readers still showing the old content, and its batch commits when the data lands — but the boundary's swap to its fallback is not part of that batch: it is applied at the flush's finalize, mainline, past any transaction park, so the fallback shows now beside whatever the write is still holding elsewhere on the page. Previously the swap was staged into the pending write's transaction and landed with its commit, by which point the data had arrived and the fallback never showed whenever any other reader of the same data existed (#3524, #3529). The children are not re-created; they stay alive behind the fallback.

    Errored accepts the same on: while it shows its error fallback, a change to a dependency clears the caught error and retries the children (reset keys). createErrorBoundary takes { on } as its third argument.

    Boundaries are exempt from A29 born-held: a Loading mounted while a transaction holds what it reads shows its fallback now (and reveals the staged content at the commit) instead of being born held with the transaction. Born held stays right for a plain memo or effect — published, its value would tear the frame — but a boundary that has not revealed is the exception by definition: its job is to catch what is not ready under it rather than let it hold. This also closes the static-vs-function-child <Show keyed> inconsistency from the issue.

  • 84562fc: LOADING_ON_OUTSIDE_HOLD now recommends the structural fix (one hold owns the data, or isPending() for the wait) and mentions latest() in on only as a capability. Docs and JSDoc for Loading on updated to match.

  • 28fcc9b: The DEV LOADING_ON_OUTSIDE_HOLD diagnostic now reports only the deterministic shape: on re-armed a Loading boundary while the very async source it is waiting on is also read by a live reader outside it, so the frame is held on that source and the fallback can never be seen (data.source names it; the fix is to move the outside read under the boundary). The after-the-fact report — the frame held by the write's action or by other pending data past the content's landing, so the staged fallback was cleared before display — is removed: that is a race the developer does not control, a fallback that loses it is a legitimate outcome, and the engine cannot tell an action that awaited exactly this data from one that awaited something slower.

  • 235173e: Prune the observability surface: OBSERVE.attribution.install, the AttributionHooks type, DEV.setConsoleFooter, the TraceSlot and OriginRef aliases, PerformanceTracksOptions.group, and the untested solid-js/refresh runtime modes (esm, webpack5, rspack-esm) are gone — @solidjs/compiler's transformRefresh({ bundler }) accepts only "vite" | "standard" to match. ownerPath() is now OBSERVE.ownerPath(subject) and diagnosticGuideUrl() is DEV.guideUrl(code); why() also accepts a scope name. Engine record types (RerunEvent, HoldEvent, …) live on solid-js/attribution only, and InteractionRef/NavigationRef on the main entries only. @solidjs/diagnostics types its record tables off the runtimes' own catalogue (RecordEvent<K>), adds the recovery table, and bumps the artifact formatVersion to 8.

  • 384a631: One records channel: the attribution engine's records (rerun, create, effect, flush, flight, fallback, interaction, hold, navigation, graph) are RecordTypes entries delivered on OBSERVE.records.subscribe(type, (event, live) => …), with the live node beside each record. Removed attribution.subscribe (both overloads), OBSERVE.subjectOf, the AttributionRecords/AttributionRecordType types, and the isSilentHold/isLongHold helpers — HoldEvent now carries silent and long, computed at settle. attribution.history(), waterfalls(), holds(), navigations() and interactions() collapse into attribution.history(type). DiagnosticListener receives the subject as its second argument. The channel allocates nothing per emit (copy-on-write listener lists), and a RerunEvent is built only while a listener, a fold or the log wants it. Record listeners belong to the channel and are no longer dropped by attribution.disable().

  • fd36d37: @solidjs/web/performance-tracks: Solid's records on the Chrome Performance panel

    enablePerformanceTracks(options?) paints the attribution engine's records — re-runs (Effects/Memos, coloured by self time, warning for a provably wasted run), interactions (input delay, handler, settle by outcome), holds (warning for a silent hold, error for a long one — the engine's own verdicts), navigations (named by route) — and the web runtime's server-function call and frame records (Server) as custom tracks in the group Solid, through the panel's extensibility API. Every span is emitted retroactively from the record's own performance.now() stamps; labels come from the shared formatters (formatOrigin, formatRerun, ownerPath), so the timeline agrees with the diagnostics artifact by construction. Dev builds emit performance.measure entries with detail.devtools (why-chain tooltips, cause/deps/blocker properties; entries cleared in batches); observe builds emit console.timeStamp spans with a 0.05ms floor and scrub value previews and non-button element text. Prod builds fold the module to a no-op. Options: attribution (the engine hold's options, log: false by default — asking for no console log, which quiets it only while no other holder wants it), minMs, rich, group, scrub. Returns the release of this adapter's hold — the engine's and its own.

    solid-js now exports ownerPath (already public on @solidjs/signals) — the root-first owner labels of a subject — on both the client and server entries, so in-process consumers of the records (this adapter) can label a re-run by its component path without importing signals directly.

  • fd36d37: Performance tracks: findings as markers, JSX-site stacks, richer properties

    • Every DiagnosticEvent delivered while enablePerformanceTracks() is on becomes a marker on the Performance panel's Timings track (SILENT_HOLD — <App> › <Search>), coloured by severity; at warn or worse it is annotated as a performance issue (detail.devtools.performanceIssue) for the Insights sidebar, linking the repair guide's section for the code. Under the scrub only the code, kind and owner travel.
    • In dev, the component wrapper stores a console.createTask(label) task on the component record (_component.task) for components rendered while an attribution engine is installed (a session with nothing enabled pays nothing), and every span and marker is emitted inside the nearest component's task, so the entry's stack in the panel points at the JSX site that rendered the component.
    • Rich mode adds Owner path (the unfolded runtime path), Node id and the root write's Origin to every node span.
    • solid-js exports diagnosticGuideUrl(code) — the repair guide URL the console footer already prints.
  • fd36d37: @solidjs/web/performance-tracks: a Propagation track (replacing Scheduler)

    Nothing re-renders in Solid, so React's component flame has no counterpart here; the picture a Solid developer wants is the graph a write travelled. The Propagation track paints each scheduler drain as a wave named by the writes that started it and what they reached (count 0 → 1 — click on button#next · 5 runs, 1 unchanged), and every run inside it — compute runs, creation runs, effect callbacks — at its own time, labelled by what made it run (<TodoRow> › effect ← doubled). The panel stacks the runs beneath their wave by time, so a wide flat wave is a coarse signal everyone depends on, a deep one a chain of memos, and a warning node with nothing after it the equality cutoff at work; a wave that mostly re-ran unchanged nodes is itself a warning. The wave span ignores minMs, so a fan-out of runs too small to paint still reads as its count.

    Node labels fold framework structure into what the developer wrote: a flow control's own nodes (<Show>'s condition value / condition / value, a boundary's children / boundary / value, <Switch>'s conditions, <Reveal>'s reveal order) present as the tag, and a primitive.local name (the store convention, and what the compiler will emit for a composed primitive's internals) as the primitive — with the runtime's name kept in the span's Node property. Presentation only: the records are what the engine delivered.

    Engine: ChangeRecord carries nodeId (the written signal's, or the changed memo's — the same id space as RerunEvent.nodeId), so a derived cause joins the run that produced it and repeated writes to one signal join each other after the record has left the process. @solidjs/signals' boundary nodes and <Switch>'s condition-builder memo are now named in observe builds (children, boundary, value, reveal order, conditions) where they read as anonymous computeds in owner paths before.

  • 757d1eb: Performance tracks: server spans in the browser panel (Stage 5)

    Dev and observe servers now write the request's timed work as Server-Timing metrics beside the trace entries: solid-invocation;dur;desc="<id>" on a server-function response, and solid-shell;dur plus one solid-boundary;dur;desc="<owner path>" per boundary that waited and settled before the shell on a document. Dev builds always write them; observe builds only while something on the server is subscribed to the invocation / boundary records (no wire change without an observer); prod builds carry no code. desc values are sanitized to printable ASCII (a non-ASCII header value throws from Headers.append, which used to be able to hang a response whose first write carried it).

    @solidjs/web/performance-tracks reads the metrics back: a call record's response headers become <id> · server spans beneath the call, placed against the resource's responseStart (via PerformanceObserver for late-arriving entries, centred in the call when none arrives within 30 s), and the document's PerformanceNavigationTiming.serverTiming paints shell · server / boundary … · server when the tracks enable.

  • 974506c: The solid-js/refresh HMR memo is now framework plumbing to the observe tiers: an internal _plumbing memo option (CONFIG_PLUMBING) leaves it unnamed, out of every owner path, and unrecorded by the attribution engine — no creation or re-run record of its own — while the component body it runs stays fully observed. Replaces the empty-name workaround from #3629, which still produced a blank-named create record per component mount.

  • e77087a: The solid-js/refresh HMR wrapper no longer appears in owner paths. Its memo is nameless, so a component reads as <App> › <Router> in performance-track labels, diagnostic findings and captures rather than <App> › [solid-refresh]App › <Router>; the proxy also carries the component's own name, so an unlabelled createComponent opens a <App> root rather than <[solid-refresh]App>.

  • dd53561: "render" record and AttributionOptions.values — the two remaining places where the observe surface duplicated a record or scrubbed one after the fact.

    "render" record (@solidjs/web, server). A server render — renderToString or renderToStream — is now a record on OBSERVE.records: RenderEvent { mode: "string" | "stream", at, shellMs?, durationMs, boundaries, outcome: "complete" | "abandoned" | "error" }, delivered when the render ends, with RenderLive { event?: RequestEvent, trace: TraceContext } beside it. shellMs is render start → the shell complete (the stream's shell handed to the sink; the string's document assembled); boundaries counts the <Loading> boundaries the shell waited on. Types RenderEvent, RenderLive, RenderListener are exported from @solidjs/web.

    The response's Server-Timing metrics are now strictly projections of records, one gate each (observed(type) || dev): solid-invocation from the "invocation" record, solid-shell from the "render" record's shellMs, solid-boundary from each "boundary" record the shell waited on — computed from the record objects at head commit, no second push. Wire format unchanged. Behavior change (observe tier): solid-shell now rides the "render" listener, not the "boundary" listener; an observe deployment that subscribed to "boundary" alone keeps its solid-boundary metrics and needs a "render" subscription for solid-shell. Dev builds still write all three always.

    AttributionOptions.values: "full" | "labels" | "none" (@solidjs/signals, re-exported by solid-js/attribution). One engine option governs the user-data fields of the engine's records at the source: ChangeRecord.prev/value, HeldWrite.prev/value, ChangeOrigin.target (and so InteractionEvent.target, HoldEvent.interaction.target), and every sentence built from them (formatRerun, formatOrigin, SILENT_HOLD/LONG_HOLD, OPTIMISTIC_REVERTED). "full" is today's dev output; "labels" drops value previews and keeps element text only on a button or an a; "none" drops both. The default is the build tier's: "full" in dev builds, "none" in observe builds (folded at build time — the observe engine ships "none" only). Across holds the least permissive level wins; a holder naming no level asks for the tier's default, so in an observe build it tightens to "none" beside anyone, while a single holder passing "full" there gets "full"; an explicit "full" never loosens what another holder demanded. Observe-tier consumers that export records should pass their level explicitly and treat it as their export contract.

    Removed: PerformanceTracksOptions.scrub and the adapter's scrub helpers. @solidjs/web/performance-tracks paints what the engine put on the record: an observe build's tracks inherit "none" (tighter than the old scrub — no element text on buttons/links either); the old observe posture is enablePerformanceTracks({ attribution: { values: "labels" } }). Behavior change: a finding's marker always carries event.message.

    Internal: solid-js's server render context seam _timing became _recordBoundary(event: BoundaryEvent) — the boundary files its record, the web runtime projects the header from it.

  • 20204ec: Server scope reserves the hydration slot without formatting an id string until a child needs one; recovers most of the renderToString cost added by #3599.

  • 7742b28: SSR: one async source, every reader. A generator yields to one reader, so under a server render the serializer pumping a memo's answer { meta, progress: gen } and a memo reading answer().progress split the generator's yields between them (the client received half the sequence; the reading memo could miss V1). Every async iterable the runtime reads on the server now goes through a seat on a shared multicast of the source (shareAsyncIterable, solid-js/internal): one pump, the whole sequence for every seat, a log trimmed to the slowest open seat, the last seat out closes the source. The serializer takes its seat through the border walk (toBorderForm, formerly envelopeContainerTraces) at context.serialize — for iterables nested anywhere in a memo's resolved value on the document face, and for frame slot args — and the frame sink's first-yield tap for document-face slot args uses the same seats, so a server component reading a source it also passes across no longer splits it.

  • eb3d699: Dev: warn with UNTRACKED_READ_AFTER_AWAIT when an async computation first reads a signal, memo, or store property after an await. Such reads are not dependencies, so the computation silently keeps its old result when they change. The check attributes the read to its computation through V8 async stack traces (Chromium browsers, Node, Deno, Bun; silent on other engines), runs only in dev builds, keeps dev settle timing identical to production, warns once per computation per signal/memo and once per store, and does not blame a continuation for reads made by effect callbacks, cleanups, or action() bodies it triggered.

  • Updated dependencies [fd36d37]

  • Updated dependencies [fd36d37]

  • Updated dependencies [fd36d37]

  • Updated dependencies [43fae6e]

  • Updated dependencies [fd36d37]

  • Updated dependencies [c74365d]

  • Updated dependencies [7599885]

  • Updated dependencies [6717d35]

  • Updated dependencies [f884589]

  • Updated dependencies [1d3ec5d]

  • Updated dependencies [fd36d37]

  • Updated dependencies [873187b]

  • Updated dependencies [a124577]

  • Updated dependencies [3af4696]

  • Updated dependencies [3613c3a]

  • Updated dependencies [ed60f05]

  • Updated dependencies [739404d]

  • Updated dependencies [ab254b0]

  • Updated dependencies [ebc1b03]

  • Updated dependencies [1d3ec5d]

  • Updated dependencies [886850b]

  • Updated dependencies [3218b7a]

  • Updated dependencies [d2d7bd4]

  • Updated dependencies [7742b28]

  • Updated dependencies [40c65cd]

  • Updated dependencies [9c6c5cd]

  • Updated dependencies [5f28e7d]

  • Updated dependencies [c9e1954]

  • Updated dependencies [55779c0]

  • Updated dependencies [84562fc]

  • Updated dependencies [28fcc9b]

  • Updated dependencies [235173e]

  • Updated dependencies [f2bd662]

  • Updated dependencies [756b1b3]

  • Updated dependencies [384a631]

  • Updated dependencies [658eecd]

  • Updated dependencies [1a7d14f]

  • Updated dependencies [5351a3e]

  • Updated dependencies [cb2fa7b]

  • Updated dependencies [53498dd]

  • Updated dependencies [c8a9d23]

  • Updated dependencies [fd36d37]

  • Updated dependencies [fd36d37]

  • Updated dependencies [fd36d37]

  • Updated dependencies [310116a]

  • Updated dependencies [4b62bc2]

  • Updated dependencies [974506c]

  • Updated dependencies [dd53561]

  • Updated dependencies [e10a4ba]

  • Updated dependencies [1f40560]

  • Updated dependencies [c1b68d9]

  • Updated dependencies [eb3d699]

  • Updated dependencies [709c02b]

  • Updated dependencies [c25d69f]

  • Updated dependencies [27bb3fa]

  • Updated dependencies [9e65de2]

    • @solidjs/signals@2.0.0-rc.10

Don't miss a new solid release

NewReleases is sending notifications on new releases.