github solidjs/solid @solidjs/web@2.0.0-rc.14

pre-release5 hours ago

Patch Changes

  • ecb68a1: Binding slots: the fill runs once per occurrence, untracked, under the occurrence's owner — as a component body and a template-slot fill do. State created in the fill lives as long as the occurrence; a top-level read is a one-time read (dev: STRICT_READ_UNTRACKED, naming the fill); getters are the reactive form. Handlers and refs are read once when an element binds and go through assign, so events delegate, tuples bind and interactions wrap as in client JSX. On the server an array at a handler position is a dev finding (reason tuple) instead of being flattened; only ref merges arrays. Template-slot fills are untracked on every render path and carry the same labelled warning.

    Breaking: AttributeSlot is renamed BindingSlot, with no alias, and its return is constrained (SlotOutput<J> / SlotError<M>, both exported) so an array, DOM node, function, async value or $-prefixed key is a type error on both sides. The diagnostic code ATTRIBUTE_SLOT_POSITION is renamed BINDING_SLOT_POSITION. The fill-shape finding also names async values.

  • d4b10b5: Binding slots: a slot property placed as a child (<strong>{list.remaining}</strong> items left) is a text position. The server emits a comment pair around it, <!--_s:t=<occurrence>:<key>-->…<!--/_s:t-->, with the escaped t=0 value inside on the document face and nothing on the stream face; the client writes the text between the markers from the occurrence's render effect, and a refetch's morph keeps the client's text. Strings and numbers render; nullish and booleans render empty, as a client insert renders them. Any other value is a new client dev finding (BINDING_SLOT_POSITION, reason text-shape) and clears. The content of <textarea>, <title>, <style> and <script> is not a text position (a comment is literal text there); bind value= or a style property instead.

    Breaking: a slot property as a child used to render nothing and raise the text finding; it now renders and binds, and the text reason is retired.

  • 2eb6e00: Cancelling a server function or frames stream partway through a chunk now ends it cleanly instead of failing as a malformed stream.

    • Chunk headers must be exactly ;0x plus 8 hex digits plus ;, and payloads must be valid UTF-8.
    • A stream that fails to decode is cancelled instead of being left unread.
    • The reader frees the memory from a large chunk once it is read.
  • 721eb06: Performance tracks no longer run the attribution cost checks by default (#3739). enablePerformanceTracks() — which @solidjs/vite-plugin calls in every vite dev session — now takes a records-only engine hold, so the six cost checks (hotRuns, hotTime, wideDeps, unstableMemos, fanOut, wastedRecompute) are opt-in again. To turn them back on, pass enablePerformanceTracks({ attribution: { checks: true } }) (or performanceTracks: { attribution: { checks: true } } in the vite plugin), or take your own attribution.enable() hold. In dev builds, the insert child-resolution effect (which must track each row's resolved child, e.g. every <Show> row of a <For>) is also exempt from WIDE_SCOPE_DEPS; HUGE_FAN_IN still covers it.

  • 365f37d: Fix a hydration key miss on a dynamic() / <Dynamic> element whose prop getter mints a hydration id (a conditional expression over a signal compiles to a condition memo) and whose children include an element (#3741). The client's runtime spread inserted children before running its attribute effect, while the server's ssrElement read attributes first, so the memo and the child element took each other's ids and the child was rebuilt instead of claimed. spread now applies attributes before inserting children — the order a compiled element already uses — and ssrElement reads a children prop after every attribute regardless of key order. Observable on the client: a ref on a dynamic() element now runs before its children are inserted, as it does on a compiled element.

  • 98d35b9: Fix computed keys in compiled SSR style objects (builds without serverComponents). A computed key's name got no ;, so style={{ color: "red", [k]: v }} rendered color:redtop:1px and the browser dropped both declarations. Hydration keeps the server's inline style, so they stayed dropped. A nullish first value also no longer leaves a leading ;. Both compilers now pass a multi-entry object to a new ssrStyleProperties(name, value, …) helper, which writes the separator only between the entries it writes; a single entry keeps ssrStyleProperty.

  • b57eb2e: Streamed fragments gated on their stylesheets now reveal under a nonce-based CSP, and no longer stay on the fallback when a sheet loads before its fragment's template is parsed (#3747). The gated links carry data-dfc instead of inline onload/onerror handlers, which a strict script-src blocks; a capture-phase listener installed by the stream's fragment helpers, from a script that carries the nonce, counts them down. The content template is now written ahead of its links: a cached sheet could fire load before the parser reached the template, and $dfr dropped the reveal.

  • 3c1d512: frames: A1b + A4 (S-ref) — a slot record's {$ref}s settle at the host's write, through the response's own data table (FrameHostOptions.resolve(ref, frameId, version, current)); an undelivered key is a pending read the response's data chunk settles and its complete/error rejects (L1 — a value that never comes is an error, not a silence); a fresh mount waits for the record to settle, a mounted occurrence's prop pends and holds its value. Codec data tables are per response (keyed by frame id and version). Deleted: ServerComponentHandlerOptions.onStream, STAGED_DATA and the staged tables, FrameHost.resolve, FrameHostOptions.isContainer/FrameHost.isContainer, Frame.rebase, the frame's record dedupe (#refArgsUnchanged, #slotResolvedRefs) — a re-sent record's equality is the fill's per-prop memo's — and the frame's error/root value latches (applied state keyed by record identity). FrameHost.preview/Frame.preview lose their resolve parameter and stay: the compute-half preview is what stages a refetch's args with the transaction that read it.

  • 3c1d512: frames: a reveal applies its range (frames-rulings 2.3, A3). A revealed segment's content is applied against the store as it is revealed — its fills mount and the segments whose placeholders it carries reveal inside it, in the same flush and before a reconstructed boundary commits the content — so the frame's flush no longer retries over its segments, and a segment nested in content a pending fill holds no longer waits for a chunk that never comes. The readiness/retry model's second ledger (#revealed / #fallbackShown) is gone: a reveal's applied state is the content record it applied, a fallback's the gate it materialized, by identity, in the one applied map; whether a segment is shown is the DOM's to say. Frame.isRevealed(segment) is removed.

  • 3c1d512: frames: A4 (S-record) — the document face declares a slot record at its marker: _$HY.r["sc:slot:<fid>:<occurrence>"] is a pending promise written with the occurrence's markup and settled with the args by the record's data script (the shape a fragment's <key>_fr takes), so the adopting client awaits a record that trails its range's reveal through the value's own .then instead of polling the registry (C2 (a2) flips). Output shape: ≈ +32–38 B per document slot record (the resolver helpers are shared with the page's fragment declarations); a sync render (renderToString) writes the settled value as before. Deleted: the #2968 setTimeout re-drain poll and FrameOptions.recordsPending / FrameOptions.drainRecords.

  • 3c1d512: Frames A5′ — a deferred fragment's placeholder inside a server component's element is the frame's content by rendering, not by adoption (frames-rulings 3.3, ruled 2026-10-06).

    solid-js: the document fragment ledger's fragmentPolicy lets a post-done swap proceed when the fragment is owned — its pl-* template is in the document and the integration's ownership predicate _$HY.fa(placeholder) says so — beside the existing claimant case; no hold, no replay for owned fragments. _$HY.fr.claim / _$HY.fr.release are removed from the published ledger (_$HY.fr is { pending, subscribe }); _$HY.fa(placeholder): boolean is the new integration hook. fragmentPending now reads a revealed fragment from its swap record (_$HY.v) before its _fr stamp: the producer emits the swap script and then the _fr settle in the same batch, so a _$HY.fr.pending() read inside the reveal notification saw the revealing fragment as still pending — a page's last reveal never read as exhaustion and a waiter released on exhaustion waited forever.

    @solidjs/web (frames client): installs _$HY.fa once (pl.closest("[data-fid]"), minus elements of a boundary disposed in place — C14); deletes claimRegionFragments, the per-adoption claim set, the cascade's claim half and the release loop (the dev-only rejection report over the region's pl-* templates stays, 0 prod bytes); documentBoundary pends on the intercept's one arrival answer (awaitBoundary) and boundaryWaiters is deleted (G9). A post-done swap into server-component markup no client has adopted yet now lands at once; the adoption that follows finds it in place and reads its declared records synchronously.

  • 3c1d512: frames: a frame's error is an errored async value (frames-rulings 3.3, A7). FrameHost.landing(address) rejects with the error record at the response's :error write, so the mount's content node throws to the nearest client <Errored> — the covering <Loading> no longer releases over an empty <solid-frame>. An error after the landing (a later yield failing, a cut-off stream, a refetch's response erroring) errors the node the same way; with no client <Errored> the core halts, as for any uncaught async error. The <Errored>'s reset re-asks: an errored landing is not a landing for a fresh consumer, and the re-read opens a new flight for the same address — the handler records the call behind every address it handles or the document answered (callFor, @internal), and the client re-invokes it through the server-function registry's RPC seam, whose client half now carries createServerReference(id). A response for an address whose mounts show an error writes through instead of being staged. frame.error still records the error.

  • 9d89df7: frames: the address is an async source (S-flush) — a mount's covering <Loading> pends on the bound address's first flush through FrameHost.landing(address) (content or error), per bound address, so a switch is a new question and the superseded address's late writes release nothing (contract C17 a, c); the store is one response's — a version bump or rebind replaces it wholesale, root included (C6 a1, b2; C7 c); a called occurrence (prop#n) found without its record waits for it instead of being evaluated argless (C18); the document face's reveal cascade syncs the adopting frame — a reveal is an apply (C2 b, C4 d).

  • 7233451: frames: the assets tier (frames savings pass C5) — the head mirror a segment's assets record drives (the stylesheet gate, module and typed preloads, inline styles) leaves the eager frames client for the lazy chunk @solidjs/web/frames/assets, loaded through the tier mechanism. A segment whose assets record carries stylesheets or inline styles is not ready while the tier is not resident: the server's fallback stays on screen and the reveal happens at max(tier load, stylesheet load) — no segment reveals unstyled. Once the tier is resident only stylesheets gate; inline styles, modules and preloads apply at the record's arrival, and a segment with none of these never waits on the tier. New export path @solidjs/web/frames/assets (@experimental); InstallOptions.tiers loaders resolve TierModule (a module with or without install()).

  • 7233451: frames: the binding-slot tier (@solidjs/web/frames/bind, @experimental) — the _s:* marker parsers, consumer discovery, the morph's owned-position arms, the per-frame consumer sets and the fill's binding leave the eager frames client as a lazy chunk; assign leaves with them. A marker met while the tier is absent holds the frame under frames-rulings 3.1 (hydration-done waits; the install's flush binds). The delegated-event replay window is closed by a bare _hk the server stamps on each document-face element with an _s:on:* position (4 bytes each, nothing else stamped): the tier marks the element completed at the bind and drains the queue, so a click before or during the hold reaches the bound handler. Data occurrences now mount with the current record.

  • 3c1d512: A server <Loading> inside a server component that fails after the first flush renders the server's outcome into its fragment instead of a blank (C12 (c), frames-rulings 3.3): the nearest server <Errored>'s fallback for the error, at the <Loading>'s position (asked through the boundary error handler's new outcome mode; a <Loading> between passes the question up); with no server <Errored> the error escapes the component — the frame's own :error on the stream face (an unkeyed error chunk), a frame-addressed { type: "error", fid, error } op on the document face's sc:live channel (only the owning adopted boundary applies it) — and the position keeps the boundary's own fallback. _fr still rejects and the keyed error chunk still rides (the diagnostics). Outside a server component nothing changes (the blank the client twin renders fresh over). HydrationContext.registerFragment's resolver gains a third argument (escaped?: { frame?: string }) and the context an internal frameId.

  • 3c1d512: frames: one server sweep lands as one frame (C13). The sink collects the hole / attr re-emissions one sweep produces and emits them as one { type: "ops", ops: [...] } chunk on the stream face (one wire line) and one sc:live op of the same shape on the document face; a sweep that changes one binding emits that member alone, as before. The client maps the unit to one record map and applies it as one write — one hole pass, one frame:applied (the hole pass now announces once per flush, not once per hole). The document op log flattens a unit into its members (last value per hole). Additive wire (FrameChunk gains the ops member; RFC addendum in frame-streams-rfc.md).

  • 3c1d512: An adopted frame occurrence claims its server markup by re-entering hydration the way a streamed <Loading> resume does (frames-rulings 3.1 / 3.2, the savings plan's A2 — S-hold's window form).

    • solid-js: hydrateWindow(id, fn, scope?) is factored out of a streamed boundary's resume and reached as sharedConfig.hydrateWindow (@internal): the keys under id gathered into the registry (the captured scope pair when another hydrate() root replaced the live one, #2917), hydrating on for the synchronous window, the current owner the claim owner (a render the window forces elsewhere is a client render, #3504), the owner the window's snapshot and live scope when none is open — so a write during a late claim is held and replays once the claim is over, and a late claim no longer re-marks the root's scope. sharedConfig.claimRoots (@internal) is typed: the claimant declares a range that may be detached around its window. holdBoundary stays the registration; the resume path is unchanged in behaviour.
    • @solidjs/web (frames): claimRender is the window — one createOwner({ id: prefix }) and the call — instead of a registry of its own gathered by walking the range, a hydrating flag flipped through sharedConfig's setter (which reset hydration-done and re-ran its completion from outside the runtime), and a hand-over of keys from the root registry: gatherClaims and hasPendingFragment are deleted (the window gathers by the producer prefix and always engages). adoptBoundary captures the registry/gather pair it adopts under so a claim made long after — under the frame's hold, at a fragment's reveal — gathers against the root that holds the frame.
    • @solidjs/web: gatherHydratable's prefix-scoped gather selects its keys natively ([_hk^="…"]) instead of sweeping every _hk and filtering in JS — it now runs once per adopted occurrence, not only per late resume.
  • 9d89df7: frames: a data chunk is under the store's version guard like every other chunk — a superseded response's late data lands nowhere, never in the table that is now the current response's (contract C5 a, b, e).

  • 7233451: frames: drop the three dead #slotNodes writes for data occurrences — the only reader (the zombie check) skips data occurrences, so the consumer-element arrays were built and never read

  • 9d89df7: frames (dev): a server <Loading> fragment inside an adopted server component that rejects on the server is reported — the client shows what the server rendered for that outcome and invents no error state of its own (contract C12 c, client half).

  • 9d89df7: Hydration-done counts the frames client's holds (frames-rulings 3.1, ruled): an adopted occurrence the frame has not claimed yet — waiting for its args record or a {$ref}'s data — registers as a pending boundary through the same registration a streamed <Loading> resume takes (sharedConfig.holdBoundary, internal), so onHydrationEnd and isHydrationInProgress() mean the same thing with or without server components (contract C3 a).

  • 7233451: frames: the live wire moves into a lazy tier (@solidjs/web/frames/wire, the sixth chunk). The connection a live() loop's response runs under — the open-frame count its end is judged by (a death with frames open vs a completion), the join of a second loop onto an address already live, the supersession cancel, connection.ended / cancel — the have-list ledger per mount with its encoder, and the resume shape (Last-Event-ID + X-Frame-Have) load with the chunk. The eager client keeps the arm that awaits the tier's residency before a live response's body is read, so a live connection without the chunk cannot happen; the chunk's import starts at the live() call, before its first fetch (preload-at-call, through the new optional responseHandler.onLive() hook the sf client fires), and a non-live call never loads it. One accepted degraded case: content applied before the chunk was resident kept no ledger, so that first connect sends no have-list (and no ordinal) and receives a full snapshot.

    Public surface: Frame.have?() is removed from the Frame interface (the ledger is the tier's haveOf(frame), @internal); encodeHaveList moves from frame-transport into the tier (@internal; decodeHaveList and the FRAME_HAVE_* constants stay); the @solidjs/web/frames/wire export path is added; ServerFunctionsClientConfig.responseHandler gains the optional onLive(); ServerComponentHandler gains onLive().

    Pins: consistency/tier-wire-preload.spec and server-functions-live-loop.spec — the latter drives live()'s reconnect loop from the consumer's side (first-connect failure, the fail-fast 4xx set and 408 / 425 / 429 / Retry-After, the cursor as Last-Event-ID, the online wake, return() and a caller's signal at every phase, live(GET(fn)), the handler's local answers), closing the loop's branch-coverage gap.

    Size: frames eager −965 min / −305 br (10.89 KB); pages −349 / −335 br eager; the wire.js chunk 1,922 / 934 (lazy; the live page loads it at its first live() call). Caps ratcheted to measured + 10 B.

  • 3c1d512: frames: a plain (non-live) server component response ends at a streaming bound. A response whose content reads a standing source — a generator memo, a projection — used to stay open until the source settled, which for a source that never returns was never, with none of live's reconnect semantics. The producer now ends it after maxYields emitting sweeps past the first flush (default 64) or maxDurationMs after the first flush (default 30 000 ms) — and when the request's signal aborts after the first flush — emitting { type: "complete", bound: "yields" | "time" } before the body closes, and tears the render down quietly (sources returned, no abandonment finding). Both are new options on FrameStreamOptions (renderServerComponent, renderToFrameStream, serverComponentResponse); a live response is never bounded. The client stores :bound beside :complete and, in dev, warns once per cut-off naming live() as the declared way past the bound. createFrameSink gains an optional fourth hooks argument (onYield) and its end(bound?) takes the bound. Additive wire; RFC addendum in frame-streams-rfc.md.

  • 1b9ceb6: Frames client: an address switch re-arms the shell gate and rebinds the frame in the pass that sees the new address (the follow's compute), not in an effect's run. Under the hold model a switch delivered while the previous switch's gate still pends lands the binding in the frame that gate holds, and an effect's run is stashed with that frame — behind the very gate the rebind would release, so a second switch mid-flight never bound to the live call. The gate signal takes ownedWrite (the re-arm and a warm rebind's release are written from the pass). Both the call-driven mount and the document-adoption face.

  • 203ab1a: Server-component content for a call a boundary is showing now lands with the transition that read it, instead of morphing in when the response arrives. A refetch, or a single-flight region, is staged: the call resolves to a binding naming the staged version, and the mount commits it when that binding reaches it. The slot args go first, under the transition, so a fill deriving optimistic intent over a server arg never reads the old arg. The markup lands at the commit. Single-flight regions show when the integration's cache takes the mutation's slice, so any cache that subscribes to flight data drives it, not just Solid Router.

    An address switch's new content lands at the same commit: the frame re-binds in the run of the effect that follows the address, while the switch gate re-arms in its pass and settles on the new address's first content through a frameless registration on the host — so a second switch mid-flight still binds to the live call, and the morph no longer lands beside siblings the transaction still holds.

    Behaviour change: a response for a showing call that no reader mounts is never shown. Previously it morphed every mount of the address on arrival.

  • 7233451: frames: C4 — the regions tier. Nested server-content regions ({$frame} slot args resolved to <solid-frame> region elements with frames bound over them) leave the eager frames client for the lazy chunk @solidjs/web/frames/regions (a new @experimental export path), loaded through the tier mechanism: the server announces regions where it mints one; a record naming a region met before the tier is resident waits — a fresh mount in the held set (the frame's hold registered under frames-rulings 3.1 on the adopt path, the server interior on screen), a mounted occurrence's new record pending in the store until the install's flush. InstallOptions.tiers now types a tier's module as the new exported TierModule (its exports are the tier's appliers; install() stays optional). Frames eager −1,078 B minified / −237 B brotli; the page scenarios −1,083 B minified; the non-SC scenarios unchanged.

  • 7233451: Frames client, residue pass 1 — the D list (dead / incidental code off the eager client; frames residue pass §2 rows 2b, 2c, 2e, 2f, 2g). documentAddress is gone: an adopted mount's address comes with its binding (every serialized reference resolves to the call's binding), and a mount with none — the per-function placeholder rendered directly — binds the function id. slotsFor keeps one dispose map: a live range binding's insert lives under the fill's own owner where there is one, else under an owner registered the same way, so a re-call disposes whichever path the previous invocation took. The zombie heuristic and the #slotNodes map it read are gone — the morph relocates ranges among siblings and recreates nothing (DR-5), so "mounted" is the frame's set. #revealSegment has one path: a frame created without FrameOptions.reveal reveals through a default seam that inserts the content at once (no boundary) — the content is applied as it is revealed, as on the boundary path — and a placeholder missing its closing comment is not revealed (dev error; #showFallback already treated it so). handler.showing no longer brands the bare per-function component: the reference a reader holds at t=0 is the bootstrap's binding, branded there. The capture arm (contentHTML + the last-unmount capture) stays: lifecycle-matrix/remount pins it.

  • 7233451: FRAME_HAVE_HEADER and FRAME_HAVE_BUDGET leave the @solidjs/web/frames client entry's export list (frames residue pass §2 row 5a). They stay exported from @solidjs/web/frames/server, where the header is read; the client sends the have-list itself through the handler's resume and nothing consumed the names off the client entry. Removed public surface, @experimental.

  • 7233451: The lean re-ask (frames residue pass §3.3). The server-function client hands its response handler the call it dispatched as a thunk — ctx.retry on responseHandler.handle, info.retry on responseHandler.intercept — the same reference, arguments, declared shape and per-call options, so a GET-declared read stays a GET by construction. The frames client's re-ask for an errored boundary's reset (frames-rulings 3.3) is that thunk: callFor(address).retry(). Gone with it: the frames client's reask installer and its reads of the RPC slot and the metadata brand, and createServerReference(id) on the client half of the server-function RPC slot (getServerFunctionRPC(), @internal), which nothing reads any more. callFor (@internal) now returns the recorded call with its retry. The responseHandler type in ServerFunctionsClientConfig documents the existing intercept seam alongside handle, both carrying retry.

  • 7233451: Frames client: every slot fill is placed by insert — the fill's output has the core's lifecycle (created under the fill's owner, claimed in place under hydration through insertExpression's claim pass, disposed with the owner) instead of a frame-side copy of it. The Solid binding's static path (normalizeSlotContent, isReactiveContent, the in-place settle test) and the frame runtime's own range writer (#replaceRange) are gone; a static fill is one insert of a non-function value (no effect created), a reactive one binds the range as before. createFrame / createFrameElement: the frame element IS the range — the never-used comment-marker range mode of the runtime is removed. Slot (FrameOptions.slots, @experimental) changes shape: a callback owns its range — it places or binds its output before ctx.range.end over ctx.existing — and its return value is no longer read (before: returned nodes were placed by the frame; undefined claimed). A slot range missing its end marker (FRAME_MARKER_CORRUPTED) is left as the server rendered it instead of being filled at the parent's end.

  • 7233451: frames: residue 4 — a pending {$ref} read is owned by the decode table. A slot record's {$ref} to a key its response has not delivered yet is answered by that response's createJSONDataTable with a promise the table owns (marked s = 0, carrying c, the callbacks run when it settles), settled by the key's data chunk through the table's own apply and rejected by close(error) when the response ends (L1 — a value that never comes is an error where it is read, not a silence); a version bump drops the superseded table and its reads. The frames host keeps only the record.pending count (a fresh mount waits for its record, the covering <Loading> pends on the landing alone) and one fan-out helper; pendingRef / settleWait / the per-store waits are gone. FrameHostOptions.closeData(frameId, version, error?) (new, @experimental) tells the integration a response ended; the shared host closes that response's table. The transport awaits prepareData before a slot chunk whose args carry a {$ref}, as it does before a data chunk, so resolve always has a table. Frames eager client −505 min / −174 br (34,272 / 11,367 → 33,767 / 11,193); the lazy decode chunk +420 / +161.

  • 3c7631a: ABANDONED_FLIGHTS no longer fires for a frames slot arg that every re-shipped record re-reads while it is still pending (#3852). Dev builds mark the per-key slot-arg memo with an internal option that the check skips; the memo's flights are still recorded and still counted in feedback().

  • 7233451: Frames: the server-announced tier mechanism (frames savings pass §2, Phase B — the seam alone; no tier is cut yet, every capability stays eager).

    The server learns at render time which frames-client capabilities a response or a document needs — it mints each feature — and announces their names so the client can start the tier's import in parallel with the content instead of at first use: on a frame stream, the X-Frame-Tiers response header (the tiers the sync render pass minted) and, for a tier first needed after the head left, an in-band tiers member on the next chunk out (FrameChunk.tiers, additive); on a document, the _$HY.r["sc:tiers"] hydration record (cumulative, re-written at each new mint) plus a <link rel="modulepreload"> per tier whose chunk URL the integration gave (frameTransformDirectResult(value, { id, args, tierUrls }) — the server does not know the client's chunks; absent, the record alone carries the name). The sink's needs(tier) runs at the mint sites: a binding-slot position read (bind), a nested server-content region (regions), an assets chunk (assets), a traced container in a slot arg (trace), a live response (wire). A sync renderToString document announces nothing (no shared render slot); the client detects.

    The client (@solidjs/web/frames): installServerComponents(host?, { tiers }) takes the tier → loader map (() => import(...), the module exporting install()), reads the document's record and starts each named load; applyFrameResponse reads the header before the body and chunk.tiers at each chunk, and a data chunk awaits the tiers it names before it decodes (its node tree needs them). A fresh mount whose tier is not resident — a data occurrence's bind, a region-carrying record's regions — waits like a recordless called occurrence: on the adopt path that wait is one more reason in the frame's registered hold (frames-rulings 3.1 — the event-replay window stays open, hydration-done waits), on the stream path the record stays pending in the store; the readiness check itself starts the load when nothing announced it (detection is the fallback), and the install flushes every live frame so the held occurrence mounts with the record it was held on. A name with no loader is resident (eager), which is every name today.

    Measured before written (edited dist copies, scripts/size): frames eager client +473 B min / +145 B br (≤ +150 budget; the plan's ≈ +100 estimate, ×2.5 pre-estimate ≈ +250); pages +478 min; every non-server-component scenario 0 B. Frames server dist +793 B min; @solidjs/web's own server entry 0 B.

  • 7233451: frames: the trace tier's update-site wait. A MOUNTED occurrence's new slot record whose args carry a { $tr } container-trace marker while the trace tier is absent (a live slot op minting the page's first trace after the shell, a refetch adding a projection arg) now stays pending in the store: the live binding keeps the args it shows, the tier load starts, and the install's flush applies the record as an args change with the marker materialized. Before, #syncSlots guarded only the fresh mount (needsTrace) and the regions tier at the update site (needsRegions), so such a record was pushed into the live binding raw — the marker read as the value. Mirrors the regions tier's update-site wait.

  • 7233451: frames: the container-trace materializer is the frames client's traces tier — @solidjs/web/frames/trace, loaded through the server-announced tier mechanism (prepareTier("trace")), so the store engine leaves every server-component page that never meets a trace (page base −6.6 KB brotli, page live −6.7 KB; frames eager −83 B). solid-js/internal/container-trace is a new solid-js entry carrying materializeContainerTrace(marker, claiming?) (the store engine reached through @solidjs/signals, the hydration dispatch withStoreHydration and the patch protocol read back from solid-js); the materializer leaves solid-js's main and solid-js/internal entries. The eager frames client keeps the trigger: the loader entry, the held-set predicate (an adopt-time record whose args carry a { $tr } marker while the tier is absent is held under frames-rulings 3.1 — its server interior on screen, hydration-done waits — and mounts with the record it was held on, a replacement applying as an args change), and the claiming hint (FrameHostOptions.revive(value, claiming?)), which keys the materializer's parked backlog on the claim again: a fresh mount reads the fold of its whole backlog at once.

  • e44b2e4: Hydration: a <Loading> around lazy() whose module is still loading when hydration starts no longer moves its server-rendered nodes once the module lands. The boundary claims those nodes on its late resume, but the enclosing insert had tracked its region as empty, so it re-inserted them, which moved connected nodes and blurred a focused input. While hydration is in progress, an insert whose region is untracked now leaves the incoming nodes in place when every one of them is already a child of its parent. Once hydration completes, inserts behave as before. Fixes #3749.

  • 01e80a6: The hydration bootstrap marks every promise the inline payload files under _$HY.r as handled when it lands, so a serialized async read that rejected under an <Errored> (which already rendered its fallback) no longer raises unhandledrejection in the browser. A consumer reading the record still receives the rejection.

  • be82cf3: Observe: one interaction frame per event, not per listener. InteractionRef takes an optional event; frames with the same event re-enter one interaction record, which stays joinable for the event's dispatch and is recorded at the next task. The web runtime keys its frames by the DOM event, and @solidjs/web exports dispatchAsInteraction(e, fn) so a listener outside the runtime (a router's document click handler) joins the frame of the same event — fn() in production builds. One anchor click is now one interaction record carrying both the onClick work and the navigation (#3754).

  • 4f67697: Breaking: only on followed by an uppercase letter (onClick, onPointerDown) is an event handler. Lowercase on* names (onclick, onmouseover) are plain attributes everywhere:

    • Both compilers compile onclick={expr} like any other attribute (setAttribute, reactive when expr is dynamic) instead of binding a delegated or native event, and SSR renders it as an escaped attribute instead of dropping it. A leftover 1.x on:click={fn} is likewise a plain namespaced attribute (it previously compiled to addEventListener(":click", fn)).
    • @solidjs/web spread/assign set lowercase on* keys as attributes, the server spread walk renders them, and useHead applies lowercase on* attributes (camelCase handler names stay skipped). ssrAttribute escapes a function value instead of interpolating its source raw.
    • @solidjs/html and @solidjs/h elements wrap a function passed to a lowercase on* in a getter like any other attribute; only onXxx and ref are exempt.
    • @solidjs/html components follow @solidjs/h's rule: every zero-argument function prop, onXxx handlers and ref included, is a getter, so a component handler must declare its event argument (onClick=${e => …}).
    • New dev-only check LOWERCASE_EVENT_ATTRIBUTE (added to the DiagnosticCode union): warns once per attribute name when a function is set on a lowercase on* (or on:) attribute, naming the camelCase handler to use.
  • 41fdf96: reportRequestFailure(error, event) (@solidjs/web, server) reports a failure that fails a request outside any render or server function the runtime reports, such as a middleware throw that a framework's request handler catches. The ambient server error hook hears it as kind: "request", handling: "failed", with the request event, once per error object, and its return is ignored. With no hook registered the failure goes to console.error, as a render that fails before its shell does. ServerErrorSite.kind (solid-js) gains "request". On the client the function is a no-op.

    A synchronous throw out of renderToString, or out of renderToStream's first pass (and so renderToFrameStream and serverComponentResponse), is now reported to the server error hook as kind: "render", handling: "failed" before it is rethrown, where it previously reached the caller without the hook hearing it. A request handler that catches it and calls reportRequestFailure adds nothing; the hook hears it once, as the render's.

  • 53ef0e6: Single-flight delivery now settles slice entries the server folded while still pending before handing the envelope to the flight-data consumers. A collector may fold values that are still in flight (the router's collector returns its preloads' query promises as they stand), and the codec streams those after the response head; since the body decode resolves on the first chunk, the mutation call resolved, and a router action settled (busy state cleared, onSettled ran) before the refreshed route data existed. deliverFlightData, shared by the plain client and the frames client, now waits for every pending entry (allSettled, so a failed read stays with the cache entry it seeds) before running consumers.

  • 2656284: createSSRResponse no longer hangs when a streamed render ends before its shell flushes (#3719). A render that fails pre-shell (onError hears handling: "failed") or is aborted through its signal now resolves with a bodyless 500, or a redirect when a Location is already on the response stub, and the stub is committed. A render that succeeds with an empty document resolves with an empty 200. The promise still never rejects.

  • 9338c00: createSSRResponse over a stream result now tears the render down when nobody will read it (#3768). Cancelling the resolved body (the client went away, a HEAD request whose body the host cancels) disposes the render through renderToStream's disconnect path, as cancelling its readable does: every async source still being pulled is returned, onCleanup runs, and nothing more is written. This emits SSR_STREAM_ABANDONED with data.reason: "consumer". A pre-flush Location (a bodyless redirect) tears the render down the same way, as the JSDoc already said, but without the finding, because nobody left. Previously both only dropped writes, and the render ran on until its sources happened to end. The render's observe record settles as "abandoned" in both cases.

  • 7addcc6: Fix two SSR failures when the shell suspends above an <Errored> (a lazy memo throwing NotReadyError, the router's flash-decode shape). A <Loading> below the boundary whose content settled before the shell's suspension did lost its content: the fallback shipped and nothing swapped it, because the fragment inlined into a shell that did not yet hold its placeholder. And a hole that reached a suspension through a returned accessor ({props.children} resolving to an <Errored> whose subtree is pending — a lazy() route with no <Loading> between, for one) re-read its expression on every retry, re-creating every component above the boundary — state and pending sources included, so a component owning its pending source never converged. The retry now resumes the accessor that suspended.

  • 6f77b1b: Fix the end of a streamed render racing a suspended shell. A <Loading> fragment settling while the shell was still suspended (a lazy memo throwing NotReadyError above it) could empty the fragment registry before the shell's root holes re-pulled, and the render flushed its serializer then: everything the re-pull serialized was dropped from the hydration data, and the awaited form (renderToStream(...).then) completed with an empty document — or, with an <Errored> holding the suspended subtree, never completed. The end of the response now waits for pending root holes.

  • 537e945: SSR validates dynamic attribute and tag names: a spread key the HTML parser would not read back as one attribute name (whitespace, quotes, <, >, /, =, control characters, or empty) is dropped, as the client's setAttribute refuses it; other names are written as is (& is no longer escaped to &amp;, which the parser keeps literally in a name). A <Dynamic>/ssrElement tag that is not one element name throws, as the client's createElement does.

  • 7233451: Add dynamicComponent, the component-only sibling of dynamic (client and server entries). Same contract, semantics and hydration shape as dynamic — the two share one implementation — but its source type excludes tag names, and it never references the element runtime: dynamic must be able to render a tag, so one dynamic on a page retains createElement, spread, the prop-collection helpers and the SVG/MathML tables for everyone; dynamicComponent never does. It is the documented way to mount a server component (dynamicComponent(() => getStory(id))); a server-component page mounted through it sheds ≈ 2.2 KB brotli. dynamic is unchanged.

  • 8d66ae5: Server bundles that don't serialize no longer retain the serializer's plugin set. Five module-level statements that Rollup shakes but Rolldown (Vite) and esbuild keep are now side-effect free to every bundler: the frozen DEFAULT_WEB_PLUGINS array is /* @__PURE__ */-annotated, seroval Feature flags are read on use, the stub gap-fill header set is built on first use, the event-stream heartbeat's new TextEncoder() is annotated (and no longer built at import time by dist/server.js), and the flash-cookie matcher is a regex literal. A server bundle importing only isServer or getRequestEvent drops from ~11.7 KB to ~1.6 KB minified (seroval bundled; Rolldown), and renderToString bundles shed ~260–380 B. No behavior or API change.

  • Updated dependencies [4b4c2cb]

  • Updated dependencies [21ff8f8]

  • Updated dependencies [3c1d512]

  • Updated dependencies [9a213bb]

  • Updated dependencies [b0c6e6a]

  • Updated dependencies [da84bd9]

  • Updated dependencies [3c1d512]

  • Updated dependencies [3c1d512]

  • Updated dependencies [3c1d512]

  • Updated dependencies [9d89df7]

  • Updated dependencies [7233451]

  • Updated dependencies [759a9b6]

  • Updated dependencies [e345dc7]

  • Updated dependencies [41fdf96]

  • Updated dependencies [87a3862]

  • Updated dependencies [7addcc6]

    • solid-js@2.0.0-rc.14

Don't miss a new solid release

NewReleases is sending notifications on new releases.