octane
0.14.3 · 21 Oct 2026MIT19k / weekRepositorynpmAtom feed
105 stable releases, newest first, by release line. Notes are quoted as the maintainers published them.
- 1.x
1.0.3
3 Jun 2015Reading the note…
1.0.2
3 Jun 2015Reading the note…
1.0.1
1 Jun 2015Reading the note…
1.0.0
1 Jun 2015Reading the note…
- 0.14.x · latest line
0.14.2
21 Oct 202687bf841: Reduce context dependency allocation for consumers reading one or two contexts while preserving memoized provider updates.
9eede44: Resolve
useFormStatusthrough portal source frontiers on the first render.1b42344: Preserve server-rendered input state when a pending query remains current across an unrelated hydration update.
Patch Changes
87bf841: Reduce context dependency allocation for consumers reading one or two contexts while preserving memoized provider updates.
9eede44: Resolve
useFormStatusthrough portal source frontiers on the first render.1b42344: Preserve server-rendered input state when a pending query remains current across an unrelated hydration update.
1a278e0: Allow fixed textarea binding views with one verified renderer-owned attribute spread to transfer their early binding lease during hydration.
0.14.1
21 Oct 20268766299: Preserve server-rendered DOM when hydrating component props containing conditional render blocks.
3b1610d: Restore hydration handoff for complete fixed DOM binding views with pure setup locals.
b6f8ff4: Allow early DOM bindings for a complete fixed native view to hand ownership to hydration without requiring a structural binding program.
Patch Changes
8766299: Preserve server-rendered DOM when hydrating component props containing conditional render blocks.
3b1610d: Restore hydration handoff for complete fixed DOM binding views with pure setup locals.
b6f8ff4: Allow early DOM bindings for a complete fixed native view to hand ownership to hydration without requiring a structural binding program.
bc93c30: Restore focused hosts that were disconnected or moved to another document during an update.
5b641bd: Keep children of an incomplete pending primary usable after a suspended root render is rolled back.
b5794e8: Preserve setup locals captured by keyed Fragment children during server rendering.
80d230b: Preserve destructured rows backed by one-shot iterators in universal
@forloops.a992b16: Prevent handlers from discarded suspended hydration attempts from receiving events during rendering. Custom-element listeners register when resumed hydration is accepted, which can change their order relative to third-party listeners registered while rendering.
81d1181: Preserve pre-hydration input edits through unrelated updates to suspended boundaries with qualified layout callbacks or a stable optional reader.
238f4fb: Reduce ancestry copying when retrying suspended signal owners inside deeply nested hookless components.
4550907: Reduce ancestry-path copying when retaining and reclaiming signal owners during suspended render retries.
2778142: Fix conditional content in replaced keyed rows with many dynamic bindings.
119f105: Preserve server-rendered inputs during suspended hydration when an unrelated parent update leaves the boundary's captured inputs verifiably unchanged.
0640f0e: Allow fixed textarea binding views with props-object parameters to hand off an early signal control to hydration, including a textarea nested in an owned native view.
0.14.0
20 Oct 2026175b85d: Render computes, commit writes, for compiled template bindings. An update now
records changed text, attribute, class, style, event-handler and
controlled-form values, and a commit function the compiler generates per
template writes them at commit, before refs and effects. A transition or
other render that is dropped never touches the DOM, and an event during a held
transition calls the committed handler.As in React, an update coerces attribute and style values while it commits, so
an error from a value'stoStringreaches the nearest error boundary as a
commit-phase error instead of a render error. Mounts still write as they render.175b85d: A dormant
<Hydrate>island whose captures or provided context values changed
before it activated now hydrates first, as React hydrates a dehydrated boundary
before the update that reached it, instead of rendering on the client. The
newer values then apply as an update in a render of their own. This changes
what an island observes when a parent update reaches it before activation:An update that reaches an island while its hydration attempt waits on data
still leaves the server HTML in place until the attempt retries, an intentional
difference from React thatdocs/differences-from-react.mdrecords.175b85d: A descriptor list item (an array of
createElementor.tsxchildren) now
serializes without a comment range of its own whenever it renders exactly one
host element and no component or portal inside it, as a compiled@forrow with one
element does. A host whose children are other elements or arrays no longer
gets a range. Hydration decides the same from the item's value, so an item
that arrives in a range it does not need, as from an older server, is a
structural mismatch: the nearest Suspense or<Hydrate>boundary, or the root,
renders on the client and reports one recoverable error.
Minor Changes
175b85d: Render computes, commit writes, for compiled template bindings. An update now
records changed text, attribute, class, style, event-handler and
controlled-form values, and a commit function the compiler generates per
template writes them at commit, before refs and effects. A transition or
other render that is dropped never touches the DOM, and an event during a held
transition calls the committed handler.As in React, an update coerces attribute and style values while it commits, so
an error from a value'stoStringreaches the nearest error boundary as a
commit-phase error instead of a render error. Mounts still write as they render.175b85d: A dormant
<Hydrate>island whose captures or provided context values changed
before it activated now hydrates first, as React hydrates a dehydrated boundary
before the update that reached it, instead of rendering on the client. The
newer values then apply as an update in a render of their own. This changes
what an island observes when a parent update reaches it before activation:The island's first render runs with the captures and context values its
server HTML was rendered with.Its effects mount with that first render's values, then update.
A callback ref that the update replaces goes from the server node to
null
and back to the node.In development, server attributes that differ from the captures the island
was claimed with now log the "won't be patched up" warning, as in any other
hydration pass, and a structural difference from them is reported as a
mismatch.The update patches the adopted server nodes, including any draft the user
typed in them, instead of replacing them with client-rendered ones.
An update that reaches an island while its hydration attempt waits on data
still leaves the server HTML in place until the attempt retries, an intentional
difference from React thatdocs/differences-from-react.mdrecords.175b85d: A descriptor list item (an array of
createElementor.tsxchildren) now
serializes without a comment range of its own whenever it renders exactly one
host element and no component or portal inside it, as a compiled@forrow with one
element does. A host whose children are other elements or arrays no longer
gets a range. Hydration decides the same from the item's value, so an item
that arrives in a range it does not need, as from an older server, is a
structural mismatch: the nearest Suspense or<Hydrate>boundary, or the root,
renders on the client and reports one recoverable error.175b85d: Change the server HTML that hydration reads (hydration protocol version 3). A
value a component returns now renders directly inside that component's comment
range instead of in a second range of its own, as do the values of the
components it returns in turn: a component returning<em>serializes as<!--[--><em>…</em><!--]-->rather than<!--[--><!--[--><em>…</em><!--]--><!--]-->.
A component that is a@foror.map()item's only content now keeps its own
range inside the item's, also when.map()runs another collection'smap
method. A keyed or configured<Activity>in a.tsxcomponent's returned JSX
passes its children to the server as a children block, as the client does. An
element's only child that is a signal holding text, or a ternary with a JSX arm
in a.tsxcomponent's returned JSX, serializes as the element's bare text when
its value is text, as the client renders it. On the client, a component's
returned value renders inside the component's own markers on a fresh mount
too, so it adds no anchor comments.Hydration now adopts by structure. The server frames every component call, and
a client call whose range the server did not render is a structural mismatch:
the nearest Suspense or<Hydrate>boundary, or the root, renders on the client
and reports one recoverable error, and the development warning names the call
site. So does a list, branch or text hole whose server range has another shape:
a list range without its item marker, a range around each row of a list whose
rows are one element each, a branch whose server arm is longer than the
client's, or a range where an element's only child is text. React adopts such a
tree when its DOM happens to match, as when the server rendered another branch
that renders a component's elements inline. As in React, a suspension in a
component's body still keeps the server HTML until it resolves. Every
structural mismatch, a server range without its close included, reachesonRecoverableErroras the sameHydrationMismatcherror.Every hydratable root's server output now opens with a comment naming the
protocol version,<!--3-->, whether or not it seedsuse()values, and so does
an independent<Hydrate>island's. Head metadata folded into a fragment root
comes before it, sohead + htmlunderheadChannel: 'separate'is still the
folded output. The comment stays in the DOM, as the range comments do. A root
whose server rendered another version, or named none, renders on the client and
reports error 348 throughonRecoverableError; a root that hydrates an app
container inside a server-rendered document finds the version before<html>.
Deploy the server and the client from the same Octane version.175b85d: Change the server HTML that hydration reads (hydration protocol version 2). A
Suspense boundary is now one comment range whose opening comment records what the
server rendered:<!--[$:N-->its content,<!--[$?id-->a streamed boundary,
or<!--[$!rN-->a boundary the client renders. Theoct-suspense:,oct-seed:,oct-client:,oct-client-error:andoct-native-fresh:comments
and the arm's own nested range are gone, and so is the range an@ifor@switcharm had inside its slot's. Pages carry fewer comment nodes.The server and the client must come from the same Octane version: a root whose
server rendered another protocol version renders on the client and reports
error 348 throughonRecoverableError. A presentation-binding view keeps its
own boundary and arm shapes.175b85d: Server rendering errors now follow React: the server never renders a
@catcharm or an<ErrorBoundary>fallback.A render error, or a rejected
use(), inside a Suspense boundary (a@trywith@pending, or<Suspense>) now reports throughonErrorand ships that boundary's fallback, marked for the client to render, inrenderToString,prerender, and streams before and after the shell. Previously the server rendered the boundary's@catcharm and serialized the rejection's name, message, and fields so hydration could adopt it. Now no part of the error reaches the page.hydrateRootrenders the boundary afresh from client data, starting its requests again, and reports it once throughonRecoverableError. If the client render throws too, the boundary's@catcharm or the nearest client error boundary shows it, as in any client render.A server error with no Suspense boundary above it, even inside a
@trythat has only@catchor an<ErrorBoundary>, now fails the render:renderToStringthrows,prerenderrejects, and a stream fails its shell throughonShellError. An app that relied on a server-rendered catch arm, such as a rootcatchboundary without apendingone, should wrap the content in a Suspense boundary, whose fallback the server sends while the browser renders the catch arm. An authored@try/@pendingboundary insiderenderToStringnow recovers a server error as<Suspense>did, instead of failing the render.A
@trywith only@catchno longer holds a suspension on the server, matching React's error boundaries and Octane's client. While something inside it waits, the nearest Suspense boundary's fallback ships, instead of an empty placeholder for that@tryalone, and with no Suspense boundary above it the shell waits.175b85d: Signal writes inside
startTransitionkeep their fork semantics on the same
update queues asuseState. A signal written in a transition keeps its
committed value for committed reads, subscriptions and DOM bindings, and gains
one pending value that the transition's renders and its synchronous callback
read. Those renders suspend, hold and commit with the transition's state
updates, and an urgent write rebases as a state update does. Two transitions
that write the same signal now commit together, as React entangles transitions
that update one queue; a later transition no longer replaces an earlier one's
write. A transition whose boundary shows its timeout fallback renders the rest
of its work in the next task, after the fallback commits.Apps that use signals with transitions ship about 9.4 kB raw and 2.8 kB gzip
less framework code. Compiled DOM binding views and their controls drop the
transition preview path that no transition reaches any more, and ship 0.3 to
1.2 kB gzip less.175b85d: A streamed Suspense boundary whose content has not arrived when hydration
reaches it now keeps the server's fallback until the stream swaps the content
in, as React leaves a pending dehydrated boundary, instead of rendering on the
client at once. The swap then hydrates the server's content, adopting its nodes
anduse()seeds. An update that reaches the boundary first still renders it
on the client, and a boundary the stream marks errored ($OCTRX) renders on
the client and reports once throughonRecoverableError. The stream's swap and
error scripts call a retry the client leaves on the boundary's open comment, so
deploy the server and the client from the same Octane version.
Patch Changes
069aa15: Upgrade
devalueto 6.0.3 for faster serialization and improved malformed-input handling. Octane declares theFloat16Arraytype devalue's typings name, so strict consumers on an ES2024libkeep compiling.175b85d: Register compiler commit writers by cell without searching and sorting the plan.
59e83ac: Reduce generated component code for native signal reads, recognized fixed-field attribute spreads, and signal-backed child bindings. Known attribute spread providers can opt into additional savings by declaring that successful returns are non-null.
6ad2083: Reduce generated code for template helpers that capture multiple values.
2c635ad: Move compiled Router failure presentation into the Strong match tree and preserve synchronous error reporting through the owning catch arm.
6d1f75e: Avoid repeated whole-module scans when proving single-root components across conditional component chains.
bfccf26: Binding views can forward a closed parameter rest to a child view, adopt a native root inside a child's
@ifarm, and hand off composed native buttons tohydrateRoot. That includes closed-restdisabled,hidden,type,command,commandfor,popovertargetandpopovertargetactionchannels, unbound provider spreads inside structural views, and hosts behind pureconstaliases. Renderer updates to a presentation view's host spread now write at commit.b8b772a: Type-check dynamic
.tsrxtags against the props of the selected element or component.175b85d: A
'use dom bindings'view no longer writes the page while an Octane render is
running. When render code publishes to a binding's source or calls itsrefresh(), the binding writes once that render returns, whether the root
commits it or drops it, so the next paint shows the new value as before.
Outside a render, notifications andrefresh()still write synchronously.d57645f: Keep unwrapped Effect Event updates independent of pending Actions. Wrap an Effect Event in
startTransitionto join a pending Action explicitly.175b85d: Float resource elements (
<link rel="stylesheet" precedence>,<style href precedence>and<script async src>) now reachdocument.head
when their render commits, as React DOM's do, instead of while it renders. A
render that never commits, or an arm that fails, inserts none of them. As in
React, a stylesheet resource the document lacks starts loading while it
renders, through a<link rel="preload" as="style">. Resource hints
(preload,preinit,preinitModule,preloadModule,preconnectandprefetchDNS) are unchanged and insert when called.175b85d: Prevent keyed reorders from failing when the same update has already removed a focused link. Keep focus preservation through shadow roots while ignoring URL hosts on detached anchors.
175b85d: A style update to a host that
<Activity mode="hidden">or a showing Suspense
fallback hides now reaches the host at its render's commit, under the hide,
instead of while the render runs. A render that never commits leaves the hidden
host untouched, and a reveal in the same render writes the new style once the
hide lifts. A style value that fails to coerce at that commit reaches the
nearest error boundary, as other commit-time binding errors do.175b85d: Avoid redundant scoped-style validation when compiling style-free modules with multiple split hydration boundaries.
175b85d: Hydrating a Suspense boundary, a
@tryor anErrorBoundaryno longer inserts
comment nodes into the server's HTML: the boundary's content keeps the server's
range. A fallback that later shows beside that content adds one comment with
its own nodes, when it commits.175b85d: A Suspense or
@tryboundary whose hydration suspended now retries alone when
its wakeable settles: only the boundary renders again, over its server arm, with
the inputs and context it holds, and its parent component does not run again.
A retry inside a hidden<Activity>stays hidden. An error that a resumed
boundary's render throws, and that no boundary catches, now reaches the root'sonUncaughtErrorinstead ofconsole.error, and the root unmounts, as React's
does. A root reports that error alone, not the hydration failure it replaced.175b85d: Hydrate an empty only-child range without replacing the server-rendered root.
175b85d: A hydration attempt no longer changes the server's HTML while it renders.
What it would have changed waits for the commit of the attempt that completes:
the Text for an empty server text, a Suspense,@tryorErrorBoundary
boundary the client renders instead of the server's arm (one the server could
not finish, a streamed one whose content has not arrived, or one whose content
throws on the client), a<Hydrate>island or root that falls back, and the
client render that replaces them. A dropped attempt, such as one that
suspends, leaves the page exactly as the server sent it, with no mutation for
aMutationObserverto report, and its retry adopts the server DOM afresh.175b85d: A root whose returned component's server markup has no hydration range, which
only a server other than Octane's renders, no longer adopts that markup as if
it had one: as for any other markup that does not match the client render, the
root renders on the client and reports one recoverable error. Octane's server
renders a root's returned component in a range of its own, which hydration
adopts as before.da83e7a: Preserve keyed list order and surviving rows when inserting a row suspends.
12e2417: Reduce nested streaming SSR cleanup work while preserving boundary output, abandoned fallback cleanup, and resolution-wave delivery.
175b85d: Reduce compiled-list bundle size by keeping host-upgrade bookkeeping in the descriptor renderer.
996a44e: Report
OCTANE_NATIVE_SIGNAL_IMPORTwhen the compiler can identify a native signal read during rendering in a module without a signals runtime import. Addimport 'octane/signals';to the consuming module when no named signals API is needed.3c8272e: Avoid quadratic Promise memo lookup when universal and native renderers suspend and retry many sibling components. Preserve each request's owner identity and dependencies while releasing the lookup index after rendering.
175b85d: Preserve server-rendered input state when a custom mapped result is reused or a server render starts inside another render. Respect an incompatible root protocol even when the surrounding document uses a compatible version, and stage a markerless host's keyed replacement until commit.
175b85d: Reduce client bundle size when applications do not register capture-phase event handlers.
d91942b: Preserve pending signal work when a transition accepts a snapshot or latest-value presentation.
175b85d: A server render that reads a pending or failed query through
snapshot()no
longer throws "A completed native server read has no serializable ready value"
once the server has loaded a module that reads signals. The snapshot renders
the state as it is and adds nothing to the hydration snapshot, rather than an
invented ready value: the client resolves that state itself, or takes the
result a stream delivers, as it does when no module reads signals. A completedget()of a value that is not ready still throws.175b85d: Remove a Provider's previous children when they become
nullorundefined, for both a.tsx<Context value>andCompiledContext. They used to stay mounted, with their effects and state, until the Provider rendered children again.175b85d: Validate serialized retained signal owners directly, without retaining optional transition operations through the graph bridge.
ab18fa6: Avoid controlled form warnings from controls discarded before commit.
1924a92: Commit Router Link's transition cue through Octane's scheduled interaction work before navigation callbacks, while leaving unrelated deferred transition work scheduled.
217ce34: Accept genuine same-origin iframe AbortSignals for browser server-function calls while continuing to reject signal lookalikes.
175b85d:
renderToString(<App />)and the other server entry points render a component
element as its component's root, sohydrateRootwith either entry form adopts
it.175b85d: Render
@catchand<ErrorBoundary>fallbacks for server render errors, including rejected suspended work.@pendinghandles suspension rather than errors, so applications can use the same error boundary on the server and client.175b85d: Shrink apps that never use signals by about 4.4 kB raw (1.6 kB gzip). A renderable child now looks for a signal handle only once the signal engine has loaded or a module has enabled signal bindings, so a bundle without signals drops that detection and the code behind it. A handle passed through an opaque child still renders and updates, including one from a lazily imported module.
175b85d: Reduce signal and transition bundle overhead by sharing graph flag constants directly.
175b85d: Ship DevTools inspection of signal reads only in profiling builds. Native read sources, the native read driver and retry leases now carry their inspection metadata only when Octane's profiling flag is on, which takes about 1.1 kB raw (0.3 kB gzip) out of production apps that use signals. A DOM binding still ends quietly when the scope it observes retires.
175b85d: Reduce production bundle size for applications that do not use signal ownership.
175b85d: A server render now collects the signal values it reads for the hydration
snapshot whenever the application uses signals. Before, it collected only once
a module that reads signals had loaded somewhere in the server process, so the
same page could render with or without its snapshot. Hydration then presents
the server's value and applies the client's newer one as an update, as React
hydrates auseSyncExternalStorevalue fromgetServerSnapshot: a control
keeps its server node, focus and any captured command, instead of rendering on
the client. A signal the server's snapshot of a scope lacks is read live during
hydration, suspending while it is pending, as it is when there is no snapshot.
A page that reads no signals still adds no snapshot.4a33765: Limit controlled radio restoration and signal publication to native named candidates, avoiding repeated scans of unrelated form controls while preserving external form association and native group behavior.
bb2f650: Reduce mounted component memory by reserving the initial scope slot without growing an empty array's backing storage.
1842a3a: Keep accepted derived signal declarations current when an earlier pending transition finishes.
175b85d: Validate streamed selection identities directly during registration.
175b85d: Avoid serializing scoped styles twice when streaming the server-rendered shell.
3203b52: Make published runtime and router source compile with exact optional properties,
index-signature property checks, and explicit overrides, without requiring Node
ambient types for browser-only router components.Keep fragment listener snapshots on one internal object shape while preserving
undefined native option defaults at the DOM boundary.5dc82f5: Commit Three discrete mesh-event updates before the native listener returns without flushing unrelated deferred transitions.
ed24d6b: Speed up mounting deeply nested component and context trees by caching the nearest full component owner on lightweight scopes.
9bceb81: Support Router-owned executable templates through an internal compiler capability, preserving ordinary template inertness and adopting server scripts without replaying them during hydration.
Keep permanent static hydration ranges independent of the deferred activation implementation in client bundles.
Render Router-managed scripts with Strong keyed templates while preserving inline/external execution on navigation, managed keys, CSP nonces and self-removing Start transport. Preserve server-only scroll-restoration content in a static hydration range and remove it after hydration.
175b85d: Make signal writes and reads cheaper. A writable signal, and a derived value whose result is plain and ready, now keeps only its value: writing, recomputing and reading it no longer allocate snapshot objects, which are built only when
snapshot(), a subscriber or serialization asks for one. Aset()with no subscribers runs about 3 times faster, and a write that recomputes a derived value about 1.5 times faster. Resource cells now share the same object layout as every other signal.e62eee2: Avoid rescanning every ViewTransition boundary for each element scope when finalizing a transition batch.
c144725: An urgent update (
flushSync, an event'ssetState, or a layout-effectsetState) no longer skips a View Transition whose update callback already committed. Like React, it skips only a transition that is still preparing; once the animation runs, the urgent commit changes the live DOM under it.
- 0.13.x
0.13.8
15 Oct 202619033e4: A server render that reads a pending or failed query through
snapshot()no
longer throws "A completed native server read has no serializable ready value"
once the server has loaded a module that reads signals. The snapshot renders
the state as it is and adds nothing to the hydration snapshot, rather than an
invented ready value: the client resolves that state itself, or takes the
result a stream delivers, as it does when no module reads signals. A completedget()of a value that is not ready still throws.a9dfdf7: Remove a Provider's previous children when they become
nullorundefined, for both a.tsx<Context value>andCompiledContext. They used to stay mounted, with their effects and state, until the Provider rendered children again.c6cb71a:
renderToString(<App />)and the other server entry points render a component
element as its component's root, sohydrateRootwith either entry form adopts
it.
Patch Changes
19033e4: A server render that reads a pending or failed query through
snapshot()no
longer throws "A completed native server read has no serializable ready value"
once the server has loaded a module that reads signals. The snapshot renders
the state as it is and adds nothing to the hydration snapshot, rather than an
invented ready value: the client resolves that state itself, or takes the
result a stream delivers, as it does when no module reads signals. A completedget()of a value that is not ready still throws.a9dfdf7: Remove a Provider's previous children when they become
nullorundefined, for both a.tsx<Context value>andCompiledContext. They used to stay mounted, with their effects and state, until the Provider rendered children again.c6cb71a:
renderToString(<App />)and the other server entry points render a component
element as its component's root, sohydrateRootwith either entry form adopts
it.340beda: Compile key-only Fragments and keyed component spreads in Strong DOM templates without element descriptors, preserving keyed scope cleanup, prop evaluation order, and server rendering and hydration behavior.
c54cfdd: Enable Strong mode for Router's hook and store adapters while preserving their
authored hook slots and production memo optimization. Allow manually slotted Strong
modules to use the existing memo lowering when no generated declaration cache is
required.
0.13.7
15 Oct 2026781fd29: Support the website document shell, links, route-bound hooks, and head assets through the compiled Router entry. Add matching compiled Start client/server entries and a
compiledbuild-plugin option that coordinates route generation and lazy imports while preserving ordinary Router/Start behavior.Make editor/type tooling recognize dynamic style content props as ordinary host elements, matching runtime compilation and preserving scoped CSS blocks.
ea438fb: Avoid rerendering memoized context consumers when only a sibling provider changes. Retain updates for real nearest-provider value changes and conservative invalidation for hosted, replayed, and externally invalidated contexts.
2623e7a: Preserve table sections, rows, and cells when mounting multi-root template children through a component.
0.13.6
14 Oct 202655376d8: Support checked compiled-only dynamic component and context boundaries on the client and server. Streamed document signal bootstrap validates already-parsed signal manifests directly.
Add an opt-in
@octanejs/tanstack-router/compiledentry for compiled route trees, pending and error handling, lazy imports, and matching router hooks. The existing descriptor-capable router entry remains compatible.d8e2d9f: Add
modulePackage(code, id)to the bundler compiler. It returns the package that owns a module and whethertransformcompiles the module or slots its hooks.octane analyze --strong-dependenciesuses it.1f4ccb8: Strong mode now rejects element descriptor APIs with
OCTANE_STRONG_ELEMENT_API: importing or reachingcreateElement,cloneElement,isValidElement,Children, ordescriptorChildrenfromoctane, orjsx,jsxs,jsxDEV, orFragmentfrom an Octane JSX runtime entry. Descriptors render through the generic descriptor renderer instead of compiled templates. Write template JSX, pass the component itself (icon={Plus}), or pass a() => @{ … }render function. Type-only imports stay valid.Strong
.tsrxmodules also reject a single-item@for, such as@for (const item of [value]; key item), withOCTANE_STRONG_SINGLE_ITEM_FOR. Aconstholding a one-item literal that nothing but@forheaders reads is rejected too. To remount a subtree when a value changes, wrap it in<Fragment key={value}>…</Fragment>or key its only element or component. To render one value, render it directly.
Patch Changes
55376d8: Support checked compiled-only dynamic component and context boundaries on the client and server. Streamed document signal bootstrap validates already-parsed signal manifests directly.
Add an opt-in
@octanejs/tanstack-router/compiledentry for compiled route trees, pending and error handling, lazy imports, and matching router hooks. The existing descriptor-capable router entry remains compatible.d8e2d9f: Add
modulePackage(code, id)to the bundler compiler. It returns the package that owns a module and whethertransformcompiles the module or slots its hooks.octane analyze --strong-dependenciesuses it.1f4ccb8: Strong mode now rejects element descriptor APIs with
OCTANE_STRONG_ELEMENT_API: importing or reachingcreateElement,cloneElement,isValidElement,Children, ordescriptorChildrenfromoctane, orjsx,jsxs,jsxDEV, orFragmentfrom an Octane JSX runtime entry. Descriptors render through the generic descriptor renderer instead of compiled templates. Write template JSX, pass the component itself (icon={Plus}), or pass a() => @{ … }render function. Type-only imports stay valid.Strong
.tsrxmodules also reject a single-item@for, such as@for (const item of [value]; key item), withOCTANE_STRONG_SINGLE_ITEM_FOR. Aconstholding a one-item literal that nothing but@forheaders reads is rejected too. To remount a subtree when a value changes, wrap it in<Fragment key={value}>…</Fragment>or key its only element or component. To render one value, render it directly.479118f: Compile direct inline
@{ ... }component props in Strong DOM templates as render-only bodies, without authored arrow wrappers or element descriptors. Preserve narrow dependencies for simple captured leaves, and keep setup, context, state, and cleanup owned by each receiving insertion. Ordinary JSX and blocks in other value positions retain their descriptor behavior.Expose the
render-valuesbenchmark through the MCP benchmark tool.55139ec: Strong mode now compiles only
.tsrx,.ts, and.jsmodules. A.tsxor.jsxmodule reportsOCTANE_STRONG_TSX_MODULEwhen its own"use strong"directive or the application'scompiler.strongsetting reaches it, because its JSX lowers to element descriptors that render through the generic descriptor renderer instead of compiled templates. Rename the module to.tsrxand write its markup as template JSX, with keyed@forblocks in place of.map()lists. A module without JSX can stay.tsor.js, whereroot.render(App, props)renders an entry point. Installed dependencies keep their own mode.OCTANE_STRONG_ELEMENT_APInow applies to every Strong module.collectDiagnosticsandoctane analyze --strong-previewstill list a.tsxmodule's other Strong findings next to the new code, so the conversion's remaining work shows up at once. The Strong migration recipes, the MCP server's recipes and skills, and the docs examples are written as.tsrx.0bb503f: Remove browser-translation DOM accommodation from binding adoption and text updates. Framework-owned text and closed child lists must retain their original topology; applications should opt owned content out of browser translation in their initial server-rendered HTML. Preserve existing compiled host-based text writer calls and unrelated topology validation, journaling, and performance improvements.
0.13.3
13 Oct 20261c7f7cb: Ship less signal code to the browser. Client bundles that read signals no longer
carry the server's seed serialization, which only the server's native read
collector installs: about 2.1 kB raw and 0.5 kB gzip. Behavior is unchanged.e75d7a8: Hydrating applications without a
<Hydrate>island no longer ship the island-only parts of hydration: resuming an activation that suspended, settling the island's trailing server content, and keeping the server nodes an island attempt changed. About 0.4 kB of minified runtime leaves bundles that callhydrateRootwithout islands. Behavior is unchanged.cfdb2ca: Compiled component bodies in modules that import
octane/signalsno longer wrap every render in a native-read bracket. The renderer already collects each body's reads, including reads made through helper calls, so the bracket only rejoined that collection. Signal-enabled modules compile smaller and hydrate slightly faster; their behavior is unchanged.
Patch Changes
1c7f7cb: Ship less signal code to the browser. Client bundles that read signals no longer
carry the server's seed serialization, which only the server's native read
collector installs: about 2.1 kB raw and 0.5 kB gzip. Behavior is unchanged.e75d7a8: Hydrating applications without a
<Hydrate>island no longer ship the island-only parts of hydration: resuming an activation that suspended, settling the island's trailing server content, and keeping the server nodes an island attempt changed. About 0.4 kB of minified runtime leaves bundles that callhydrateRootwithout islands. Behavior is unchanged.cfdb2ca: Compiled component bodies in modules that import
octane/signalsno longer wrap every render in a native-read bracket. The renderer already collects each body's reads, including reads made through helper calls, so the bracket only rejoined that collection. Signal-enabled modules compile smaller and hydrate slightly faster; their behavior is unchanged.3d5994c: Keep Strong-mode ref arrays and immutable object/array props stable when their
inputs are unchanged, including object literals assigned to a local before
being passed to a component. Local values used during setup or captured by
callbacks keep their ordinary JavaScript lifetime. AllowuseMemoanduseCallbackin Strong mode with inferred, explicit, or null dependencies so
authors can choose when to recompute a value or recreate a callback.292eaa1: Keep Strong literal props and ref arrays stable when they contain regular functions that do not depend on their receiver or their own identity. Functions that can mutate their receiver or themselves continue to get fresh values each render.
0.13.2
13 Oct 2026dcb376a: Fix a Suspense boundary that could stay on its fallback forever when a component reads a fresh promise on every render, such as
use(props.source.promise)through a getter. When the compiler batches such a call, the resumed render now keeps the promise the first render suspended on, as an unbatcheduse()call already did, instead of suspending again on each new promise.ffd1f91: Fix production memo regions and cached
@forlists leaving a context consumer stale when it renders through a tag bound in the component body, such asconst C = props.comp; <C />or{ comp: C }, or when a component returns an element it received, such asif (props.on) return props.content. That component now records its context reads, and a cached list refreshes rows that render such a tag or a value hole like{item.content}.1d43f07: Fix a production memo region skipping a context update below a pure callee that renders a prop value as a child, such as
{props.content}holding an element whose component reads context. The region now refreshes that consumer when the context changes.
Patch Changes
dcb376a: Fix a Suspense boundary that could stay on its fallback forever when a component reads a fresh promise on every render, such as
use(props.source.promise)through a getter. When the compiler batches such a call, the resumed render now keeps the promise the first render suspended on, as an unbatcheduse()call already did, instead of suspending again on each new promise.ffd1f91: Fix production memo regions and cached
@forlists leaving a context consumer stale when it renders through a tag bound in the component body, such asconst C = props.comp; <C />or{ comp: C }, or when a component returns an element it received, such asif (props.on) return props.content. That component now records its context reads, and a cached list refreshes rows that render such a tag or a value hole like{item.content}.1d43f07: Fix a production memo region skipping a context update below a pure callee that renders a prop value as a child, such as
{props.content}holding an element whose component reads context. The region now refreshes that consumer when the context changes.dcb376a: Fix a client-only
<Hydrate>boundary with anever()strategy that could reveal stale data. While its content is suspended, a parent update now re-renders that content with the new props, as on first mount. Before this, the update was held back, and once the old promise resolved the content could render it instead of waiting for the new one.b826221: Mount a hookless component without an anchor comment when its root is a complete
@if/@elsechain or@switchwith a@defaultwhose arms render other components, even when one of those components can render nothing. The runtime now keeps such an arm in place when the component inside it empties or swaps its element, so a host whose children are all components no longer clones and positions an empty comment for these children. Rendering, hydration and update behavior are unchanged.f628963:
useFormStatusno longer reports the status of a<form>that its own component renders as its single root element. Only an ancestor form counts, as in React.bcb89a7: Fix hot updates leaving importers with stale values when a module exports a value beside a component. Under Vite, the module now invalidates when such an export changes, so the update propagates to its importers. Under webpack and Rspack, the module no longer accepts its own updates, so its importers re-execute. Editing only a component still refreshes it in place.
c40411e: Extend the fix for stale importer values to universal renderer modules, modules that render renderer-owned regions, and
.mdxdocuments. A value exported beside their components, such as an.mdxdocument'sfrontmatter, now propagates a hot update to its importers, as it does in DOM modules. An.mdxdocument also hands each edit to the wrapper its importers already hold, so consecutive edits reach live mounts instead of only the first.c8256d8: Retain adopted server ranges through hydration instead of rewriting their owners
after every pass, and load native signal seed adoption only for signal users.
Keep streaming and early binding range interpretation consistent.96871a6: Skip unchanged imported pure components in Vite production builds. A parent now reuses an imported child's output when the child's module published a compiler memo proof and the child's props, the live imports its subtree reads, and the proofs of the imported components it renders are unchanged, matching what same-module components already do. Imports without a proof keep their plain calls.
860e865: Mount imported lightweight components without an anchor comment in Vite and Rspack production builds when their module proves the component's root keeps its position, as same-module calls already do. A host whose children are all components no longer clones and positions one empty comment per such child. Rendering, hydration and update behavior are unchanged.
1d43f07: Render hookless imported components on the lightweight component path in Vite production builds, as same-module ones already are. An exported component whose body calls no hooks, returns no value and whose subtree reads no context now publishes that proof, and an importer calls it without allocating a Block. Mounts and changed-prop updates of imported component chains do less work; rendering, hydration and update behavior are unchanged.
738e2df: Fix an
@if/@switcharm whose only output is a hookless component's inner branch. When that branch emptied or swapped its element, the arm's next swap rendered after every later sibling, or left the replaced element behind. In production builds, two such components side by side around an imported hookless component could throwNotFoundErrorafter one emptied in the same render that the other swapped arms. The arm now follows its boundary through the component.62d2fed: Keep hookless same-module components on the lightweight component path inside production memo regions when their subtree cannot read context, instead of giving each one a full Block. Mounts and changed-prop updates of such component chains do less work; rendering, hydration and update behavior are unchanged.
8cb36e6: Reduce reconciliation overhead for unchanged, index-independent keyed rows after an insertion or removal.
2b858bb: Fix a
use()call that could commit another call's value after a Suspense boundary resumed. When a promise resolved in the same task as a state or store update that changed whichuse()calls a component makes, the resumed render could hand a later call the promise an earlier call had stored. A component with an update queued since the suspended render, and everything it renders, now re-renders as that update.4e8644f: Reuse shared renderers for private same-module components with closed static output, avoiding returned element descriptors without duplicating template bodies.
Preserve live default props when returned components escape through TypeScript import aliases or forward references in returned host trees.
0a92845: Refresh context consumers inside list rows that production builds skip. When a row's item was unchanged, a
@foror compiled.map()list left the row untouched, so a component the row rendered from a value hole such as{item.content}kept a stale context value after its Provider changed. This happened whenever no whole-list cache covered the list, for example with a filtered iterable or after a keyed selection change. Skipped rows now refresh their consumers after a Provider value change and, below amemocomponent that updated its own state, keep their consumers' context reads on it, the way amemobailout does.af5c758: Render hookful components without a Block of their own in Strong production client builds. A same-module component, or a Vite import from another Strong module, whose only hooks are
useState,useReducer,useRef,useEffect,useLayoutEffect,useContext, anduseIdnow takes the lightweight component path. Its state, effects, refs, and context reads behave as before; its update re-renders the nearest component that owns a Block, which Strong render purity makes unobservable. Compatibility-mode modules keep a Block for every hookful component.f628963: Render more hookful components without a Block of their own in Strong production client builds. Besides
useState,useReducer,useRef,useEffect,useLayoutEffect,useContextanduseId, a component on the lightweight path may now calluseLazyRef,useInsertionEffect,useImperativeHandle,useLayoutSnapshot,useEffectEvent,useSyncExternalStore,useLinkedState,useTransition,useDeferredValue,useOptimistic,useActionState,useFormStateanduseFormStatus. A component that callsuse()keeps its Block.b567aa2: Render Strong components that read context with
use(Context)without a Block of their own in production client builds, asuseContextcallers already do. This applies when everyuse()call takes a context the same module creates withcreateContext. A component that passesuse()a promise, a prop, or an imported context keeps its Block.c40411e: Fix a webpack and Rspack HMR syntax error in modules that hold more than one universal renderer unit, such as a DOM module with two
<Canvas>regions or a universal module with a nested renderer region. Each region now declares its own hot-module alias instead of redeclaring__octaneWebpackHot.6b7599a: Fix leaked thread functions under Vite HMR in modules with more than one thread-owning universal unit, such as a universal module whose renderer region also uses
main-thread:handlers. Vite runs only the last dispose handler a module registers, so each edit used to unregister only the last unit's thread functions. Every unit now queues its cleanup onimport.meta.hot.data, and the one handler Vite keeps runs all of it.5594cb0: Fix Vite server builds that alias bare
octaneto the client entry, as checkout-pinned harnesses do. Vite appliesresolve.aliasbefore the plugin can routeoctanetooctane/server, so a custom hook in a plain.ts/.jsmodule ran the client runtime's hooks duringrenderToStringand threwCannot read properties of null (reading 'hooks'). Server transforms now nameoctane/serverin plain modules, as compiled.tsrxoutput already did. Builds without such an alias produce the same output as before.
0.13.1
11 Oct 20267d6f6c0: Let applications that deliver streamed signal frames directly omit unused receiver-attachment code from their bundles.
efe12e3: Vendor the Alien Signals dependency graph and move internal signal feature helpers out of classes so unused feature code can be removed from application bundles.
4bc1eaa: Stop server-rendered
<ViewTransition>from wrapping.tsxelement children in an extra hydration range, which made every such boundary mismatch on hydration and regenerate on the client.
0.13.0
11 Oct 2026974a3a6: Complete the profiling counter core with bounded accepted-commit history, delayed effect attribution, per-component counters, and counters for lists, effects, refs, boundaries, Activity, scheduler work, and discarded renders. Profiling-disabled builds erase the probes; counter snapshots now use schema 2.
b27d8db: Add opt-in TypeScript output for the web (DOM client and SSR) and the experimental Valdi compiler target, preserving authored declarations, component API types, declaration comments, and source maps for downstream typed build pipelines.
bdc21d8: Add opt-in TypeScript build handoff for web and Valdi. Preserve typed compiler output and maps for automatic custom-hook modules, and let Vite's native TypeScript transform compose maps through client and SSR builds. Document and verify real Valdi Workspace emission with separately composed authored source maps.
Minor Changes
974a3a6: Complete the profiling counter core with bounded accepted-commit history, delayed effect attribution, per-component counters, and counters for lists, effects, refs, boundaries, Activity, scheduler work, and discarded renders. Profiling-disabled builds erase the probes; counter snapshots now use schema 2.
b27d8db: Add opt-in TypeScript output for the web (DOM client and SSR) and the experimental Valdi compiler target, preserving authored declarations, component API types, declaration comments, and source maps for downstream typed build pipelines.
bdc21d8: Add opt-in TypeScript build handoff for web and Valdi. Preserve typed compiler output and maps for automatic custom-hook modules, and let Vite's native TypeScript transform compose maps through client and SSR builds. Document and verify real Valdi Workspace emission with separately composed authored source maps.
c9d4793: Publish the Valdi adapter and writer type contract through
octane/compiler/valdiandoctane/compiler. Preserve component and hook types, opaque host representations, optional text capabilities, and compiler-padded or spread hook arguments. Support Valdi's legacy TypeScript resolver and validate the shipped declarations against real compiler and Workspace consumers.440fb6f: Allow writer adapters to opt into stable authored text sites and restrict
authored host tags. Existing append-only text and tag defaults remain unchanged.
Patch Changes
64c33de: Allow fixed and addressed DOM bindings to adopt closed element children separated by ASCII-whitespace Text nodes, including whitespace relocated by browser translation. Preserve those nodes and continue rejecting unexpected content and invalid text leaves.
7dcbc75: Reduce production bundle size by cloning binding snapshots directly in each arity branch, preserving separate spread sites and rollback behavior.
4fe0867: Reduce production runtime code retained by applications without capture handlers while preserving controlled-input restoration after stopped native events.
a0c7f0e: Allow unused Effect Event and external-store commit drains to tree-shake from production bundles.
6518d90: Avoid generating async warm plans for proven context reads and providers that
add no async work to start early. Preserve authored context reads and warming
for async descendants, opaque components, and reassigned context bindings.a03d79d: Allow applications without deferred form work or manual-submit transitions to omit their commit helpers. Preserve controlled selection, autofocus, development diagnostics, and form pending lifetimes when these features are first used later.
dd8805f: Restore controlled inputs after queued hydration events and layout-triggered
updates finish. Accepted drafts survive replay, and rejected edits return to the
controlled value even when a render or commit throws.4cc2a1e: Allow applications without effects to tree-shake effect lifecycle code while preserving late and reentrant effect registration.
4ff7d0d: Reuse production component regions containing eligible effectful TSRX children when their inputs are unchanged. Preserve live imported values, refs, effect cleanup, hydration and interrupted-render retries without requiring an explicit memo wrapper.
05699de: Preserve render errors with unreadable control-marker properties and restore signal writes after their render or hydration attempt fails.
773144a: Keep existing context consumers reactive when their retained ancestor becomes a
memoized component or compiler-cached child boundary. Preserve DOM identity and
context subscriptions across suspended updates.c30f912: Recover from a render that throws a native stack overflow. Signal writes are no longer rejected with
SignalWriteErrorafterwards, a parent whose@trycatches the failure keeps its signal subscriptions, and later updates no longer count as nested inside the failed render.d7c1de0: Share the stream host budget when publishing ready query promise results, allowing input and other tasks to run between large bursts of independent query completions. Recheck request ownership after each wait so replacement and disposal cannot publish stale results.
Even a single successful query can wait for a later host task when the shared budget is exhausted, so a synchronous
renderToStringimmediately after promise resolution may still render pending. Awaitedact()now drains budget-paced query and stream publications as well as their renders; tests should await it instead of counting microtasks. Query rejections retain their existing prompt failure delivery.152e4f7: Allow editor integrations to check shared TSRX sources against an explicit portable JSX type namespace. The
compileToVolarMappingstype-onlyintrinsicsoption takes precedence over authored pragmas without changing the runtime renderer or ordinary JSX defaults.0258e74: Allow applications without portals to omit portal event-ownership preparation and teardown code. Portal registration activates these helpers before rendering or deferred publication, preserving late children, rollback, and events crossing portal removal.
36c6bf3: Keep custom-hook path resolution out of client bundles that only use direct component hooks. Nested and manually forwarded custom hooks retain the same state identity and argument behavior.
5d6d02c: Stop warning in development about React's canonical camelCase HTML prop names
such ascolSpan,rowSpan,dateTime,hrefLang,useMap,accessKey,
andpopoverTarget. These names were missing from the host-property
diagnostics table, so they were reported as unrecognized props even though they
render correctly on the client and the server. Their native lowercase spellings
(colspan,rowspan, ...) remain accepted without a warning, and a miscased
spelling such asCOLSPANnow suggests the canonical name.6101f66: Omit unused ref commit and subtree collection helpers from production bundles. Install the existing helpers when a ref queue first needs them, preserving ref timing, visibility and cleanup.
2fe1f9f: Allow production bundles with proven roots that omit options to remove unused root error callback reporting, including roots whose components return arbitrary renderable values. Preserve callback registration, recovery, and hydration behavior for roots that accept options.
54ee25f: Compile
.tsrxmodules faster. Most compiles scope-analyzed the same module
two or more times, because each compiler pass that left the module unchanged
handed it to a pass that analyzed it again. Passes now share one analysis per
module tree, which removes more than half of those walks. Compiled output is
unchanged.2075c0e: Classify compiled DOM events ahead of time so applications with static handlers can omit runtime event-category tables, while preserving dynamic registration and native event propagation.
8c09d81: Keep transition hook replay behind the existing transition capability so applications without transitions can remove it. Preserve state and reducer update order, async Actions, linked state, and deferred values.
- 0.12.x
0.12.1
9 Oct 2026114e3b8: Fix
compile()output for.ts,.mts, and.ctsmodules, including Valdi
custom hooks. The native parser returned these files without their type
information, soimport type, inlinetypespecifiers,export type { … }, andexport type *were emitted as value imports and exports that fail to link.
Parameter properties also lost theirthis.x = xassignment, anddeclare
fields became real fields. These modules are now parsed with TypeScript's own
grammar, which keeps all of that, and<T>valuetype assertions still parse.Type erasure also covers two cases that broke
.tsxand.tsrxoutput: type
arguments on a superclass (extends Base<T>), and re-exports of a name that is
only a type (import type { T }thenexport { T }). Both previously left
invalid JavaScript.7af43dc: Ship less runtime to Vite applications that do not use optional capabilities
such as ViewTransition, Activity, form actions, or native signal reads. Vite's
default production minifier can now drop the code behind each capability an
application never installs. The js-framework rows app ships 2.6 kB less gzip,
and TodoMVC 3.0 kB less. Behavior is unchanged.4f82777: Fix a keyed list row that kept showing its old props after a suspended root
update resolved. The production build's output cache for the row recorded the
update's props while the update was held, and the rollback restored the screen
but not the cache, so the retry skipped the row.
Patch Changes
114e3b8: Fix
compile()output for.ts,.mts, and.ctsmodules, including Valdi
custom hooks. The native parser returned these files without their type
information, soimport type, inlinetypespecifiers,export type { … }, andexport type *were emitted as value imports and exports that fail to link.
Parameter properties also lost theirthis.x = xassignment, anddeclare
fields became real fields. These modules are now parsed with TypeScript's own
grammar, which keeps all of that, and<T>valuetype assertions still parse.Type erasure also covers two cases that broke
.tsxand.tsrxoutput: type
arguments on a superclass (extends Base<T>), and re-exports of a name that is
only a type (import type { T }thenexport { T }). Both previously left
invalid JavaScript.7af43dc: Ship less runtime to Vite applications that do not use optional capabilities
such as ViewTransition, Activity, form actions, or native signal reads. Vite's
default production minifier can now drop the code behind each capability an
application never installs. The js-framework rows app ships 2.6 kB less gzip,
and TodoMVC 3.0 kB less. Behavior is unchanged.4f82777: Fix a keyed list row that kept showing its old props after a suspended root
update resolved. The production build's output cache for the row recorded the
update's props while the update was held, and the rollback restored the screen
but not the cache, so the retry skipped the row.4f32daf: Keep keyed
@forrows in order when a row-local binding shadows an imported component.A keyed
@forrow whose only child is an imported one-element component uses
that element as its own boundary. The compiler read the import's single-root
mark even when a row declared its own component under the same name, such asconst Row = row.id % 2 ? Pair : RowImpl. Rows that rendered two elements then
lost their boundary, so reordering or removing rows left stale nodes behind.
The mark is now read only when the tag resolves to the import itself.77571d9: Mount
memo()rows without extra comment markers.A component whose body renders one element uses that element as its own
boundary when another module renders it, including as the only child of a
keyed@forrow. Amemo()wrapper around such a component did not inherit
that, so every memoized row added four comment nodes: an item pair and a
component pair. Mounting and clearing 1,000 memoized rows cost up to a third
more than the same rows withoutmemo().memo()wrappers now carry their
component's single-root mark, in development and production builds alike,
so memoized and plain rows build the same DOM. A static-hoisting
higher-order component that copies the wrapper's statics does not inherit
the mark.9728968: Keep controlled-input event restoration out of bundles that never use controlled bindings, while preserving restoration when controls are mounted later.
e241164: Reduce key-index construction for local keyed reorders while preserving DOM identity and rollback behavior.
77571d9: Mount same-module
memo()components without extra comment markers.A
const Row = memo(RowImpl)rendered in the module that declares it now
mounts the same DOM as an imported one. A keyed@forrow or a template child
whose component renders one element uses that element as its own boundary, so
1,000 memoized rows no longer add up to 4,000 comment nodes. A localRowthat
shadows the module binding keeps the marked path, because it can name a
different component on each render.
0.12.0
9 Oct 20267d5a0df: Complete the writer compiler's optional host-text mode and add an opt-in host-ref capability.
Existing adapters keep ABI 1; opted-in modules guard ABI 2 before creating prototypes.28552d9: Keep signals writable after an empty list fills with 16 or more rows.
With native signal reads enabled, a production build mounts host-only rows into
an existing empty@forlist without the ordinary row renderer once there are
at least 16 rows. Each row that read signals natively left its read frame open,
so the signal write guard stayed active after the render returned and the next
unrelated write threwSignalWriteError(#152). Each row now opens and closes
that frame as the ordinary renderer does, including when a row throws or
suspends, and rows holding a signal handle keep updating.f934732:
hostComponentnow creates its element in the namespace of the node it is inserted into, as descriptor and dynamic host tags already do. Amotion.circleinside an<svg>, or amotion.svgin HTML, now renders as an SVG element instead of an unknown HTML element.
Minor Changes
7d5a0df: Complete the writer compiler's optional host-text mode and add an opt-in host-ref capability.
Existing adapters keep ABI 1; opted-in modules guard ABI 2 before creating prototypes.
Patch Changes
28552d9: Keep signals writable after an empty list fills with 16 or more rows.
With native signal reads enabled, a production build mounts host-only rows into
an existing empty@forlist without the ordinary row renderer once there are
at least 16 rows. Each row that read signals natively left its read frame open,
so the signal write guard stayed active after the render returned and the next
unrelated write threwSignalWriteError(#152). Each row now opens and closes
that frame as the ordinary renderer does, including when a row throws or
suspends, and rows holding a signal handle keep updating.f934732:
hostComponentnow creates its element in the namespace of the node it is inserted into, as descriptor and dynamic host tags already do. Amotion.circleinside an<svg>, or amotion.svgin HTML, now renders as an SVG element instead of an unknown HTML element.ae05de0: Fix a custom
maplist row that keeps its key but changes from a single host
element to a component. The update used to throwNotFoundError. When a root
render held such a change, it could also drop the committed row until the render
committed, or leave the old row in the DOM after it did. The row now promotes to
a marked range before its host element is retired, and that retirement is undone
if the root render is held.dbab7f5: Resume server signals below
cloneElementchildren during hydration.The server's
cloneElementdid not mark two or more positional children as
fixed siblings, as the client's does. Components inside a nested array or
Fragment among those children got a different signal identity on each side,
so hydration refetched a server-resolvedquery$and reported a mismatch.ec86041: A
@switchcase that calls a hook now gets its own render scope when it is the 33rd case or later. Such a case could take an earlier hookless case's flag, so a state update inside it re-rendered the component that owns the@switchinstead of only the case.
- 0.11.x
0.11.0
8 Oct 202637a1c14: Ship less signal engine code to applications that use
signal$,derived$,query$oruseSignal$without callingcreateScope. Owner scopes the runtime
creates for those declarations get the publicScopemethods (serialize,inspect,derived$,get,set,isPending,batchandaction) only once
the program can hold one:createScope,currentSignalOwnerandinstallSignalOwnerEnvironmentfromoctane/signalsinstall them first. Only a
debug scope records a trace.SignalOwnerEnvironmentnow carries owners asSignalOwnerIdentityvalues. A
host carrier, including one installed throughoctane/server, can store and
return the owners it receives but can no longer narrow them toScopeand call
its methods. Carriers that store owners asSignalOwnercompile unchanged.Signal applications are 0.49 to 0.65 kB smaller gzipped. Applications without
signals are unchanged.0ce371a: Count engine work in profile builds.
Profile builds now keep session totals of the work around component renders:
render attempts and their outcomes, bailouts, Blocks created and torn down,@if/@switcharms kept or replaced, Suspense fallbacks and error catches,
render-queue passes, root commits, and work discarded by rollbacks. Read them
withprofiler.counters(), or takeprofiler.snapshot()before and after an
interaction and pass both toprofiler.diff(). Snapshots carry a schema
version, build, renderer set, and recording generation;diff()rejects pairs
it cannot compare. A counter the installed renderer does not record is absent
rather than zero.Builds without
profilecontain none of this code once optimized; unminified
development output keeps the disabled branches as dead code.8444db8: Support recursive
'use dom bindings'views. A view can now render itself, or another view in the same module that renders it back, including beneath a keyed@for, so a tree's depth comes from its data:export function Tree({ node }: { node: Branch }) @{ 'use dom bindings'; <section> <span>{node.label as string}</span> @for (const child of node.children; key child.key) { <Tree node={child} /> } </section> }Server rendering and the extracted binding artifact both compile, where they previously failed with "recursive binding child programs are not supported". The artifact still loads only the binding runtime, not the renderer. Each level the data reaches is its own instance with its own text, keyed items, native handlers, mount-only effects and
@tryarms. Removing a branch retires the instances beneath it. Recursive calls use the generic program rather than specializing fixed primitive props, sodepth={depth + 1}does not unroll at compile time.Recursion that could never end is a compile error. A view that renders itself on every path, for example with a recursive call outside any
@if,@foror@try, fails with an error that names the cycle, such asbinding view Tree renders itself on every path (Tree → Tree).A local child view used at several call sites with different prop names now shares one compiled descriptor, unless it spreads a rest parameter.
Minor Changes
37a1c14: Ship less signal engine code to applications that use
signal$,derived$,query$oruseSignal$without callingcreateScope. Owner scopes the runtime
creates for those declarations get the publicScopemethods (serialize,inspect,derived$,get,set,isPending,batchandaction) only once
the program can hold one:createScope,currentSignalOwnerandinstallSignalOwnerEnvironmentfromoctane/signalsinstall them first. Only a
debug scope records a trace.SignalOwnerEnvironmentnow carries owners asSignalOwnerIdentityvalues. A
host carrier, including one installed throughoctane/server, can store and
return the owners it receives but can no longer narrow them toScopeand call
its methods. Carriers that store owners asSignalOwnercompile unchanged.Signal applications are 0.49 to 0.65 kB smaller gzipped. Applications without
signals are unchanged.0ce371a: Count engine work in profile builds.
Profile builds now keep session totals of the work around component renders:
render attempts and their outcomes, bailouts, Blocks created and torn down,@if/@switcharms kept or replaced, Suspense fallbacks and error catches,
render-queue passes, root commits, and work discarded by rollbacks. Read them
withprofiler.counters(), or takeprofiler.snapshot()before and after an
interaction and pass both toprofiler.diff(). Snapshots carry a schema
version, build, renderer set, and recording generation;diff()rejects pairs
it cannot compare. A counter the installed renderer does not record is absent
rather than zero.Builds without
profilecontain none of this code once optimized; unminified
development output keeps the disabled branches as dead code.8444db8: Support recursive
'use dom bindings'views. A view can now render itself, or another view in the same module that renders it back, including beneath a keyed@for, so a tree's depth comes from its data:export function Tree({ node }: { node: Branch }) @{ 'use dom bindings'; <section> <span>{node.label as string}</span> @for (const child of node.children; key child.key) { <Tree node={child} /> } </section> }Server rendering and the extracted binding artifact both compile, where they previously failed with "recursive binding child programs are not supported". The artifact still loads only the binding runtime, not the renderer. Each level the data reaches is its own instance with its own text, keyed items, native handlers, mount-only effects and
@tryarms. Removing a branch retires the instances beneath it. Recursive calls use the generic program rather than specializing fixed primitive props, sodepth={depth + 1}does not unroll at compile time.Recursion that could never end is a compile error. A view that renders itself on every path, for example with a recursive call outside any
@if,@foror@try, fails with an error that names the cycle, such asbinding view Tree renders itself on every path (Tree → Tree).A local child view used at several call sites with different prop names now shares one compiled descriptor, unless it spreads a rest parameter.
3557100: Run
octane/compiler/typescripton TypeScript 6 and TypeScript 7.1 or later, as
well as 5.9. On TypeScript 7,createTextTypeProjectchecks.tsrxthrough its
nativetypescript/unstable/syncAPI and proves the same text facts as on the
classic API, andvalidateNativeSignalNamesaccepts a TypeScript 7 Program. A
newtypescriptoption names the TypeScript to use. The optionaltypescript
peer range includes 6 and 7.1.
Patch Changes
7540c35: Publish an Action pending cue without waiting for native View Transition capture. Preserve captures for actual queued state, root, and native signal work, and retire consumed transition receipts after synchronous drains.
f6b6615: Stop rendering a hydrated signal reader a second time when nothing it read changed.
After a hydration commit, releasing the adopted server values rendered every component that read a signal during hydration again, in the same task, even when the live value still matched the server's. Parents render their children, so a section-level read rendered the whole section twice. Release now compares each historical read with what a live read would return:
A read whose live value is
Object.is-equal to the server value moves to the live signal without rendering. A snapshot read also needs the same status and activity metadata. Later writes render the reader as before.A reader renders again when any read changed: an early edit, a newer query result or a changed selection, a pending selection, a later stream value, or a stream that has since completed under a snapshot read.
A targeted binding (
{value$}) keeps its own live subscription, so its component never starts depending on that signal.Decoded objects and arrays are fresh copies, so a component that read one still renders again.
On the
app-frame-hydrationbenchmark, theframe-live-sectionsscenario drops from 3,078 to 1,653 renders for its 1,653 Blocks. Hydration is about 11% faster cold and 13% faster warm at 4× CPU throttle (paired head/base 0.886 and 0.873 over 20 pairs). It also allocates 680 KB less heap. The leaf-onlyframescenario drops from 1,703 to 1,653 renders.293d143: Support directive values in module-level callbacks, including arrow components returning
@try/@pending/@catchwith concise bodies or explicitreturnstatements. Keep callback parameters, local values, and lexicalthisandargumentsavailable to the rendered branches without requiring an@{}wrapper.9d4c14f: Let
'use dom bindings'views bindstarton an ordered list,valueon a list
item, androwspanon a table cell. The compiler used to reject all three: the
binding runtime did not apply React's rule that removes astartorrowspan
that is not a number, and it treated everyvalueexcept a button's as form
state. These attributes now update in place and match what the server renders,
in both adopted and constructed views. Formvalue,checkedand form identity
attributes still have to be static orunbound.b6c040c: Render each Block in its signal owner without re-entering the renderer.
With signals enabled, every Block renders in an owner of its own, so nearly every
render changes owner.renderBlockentered that owner through a callback frame
that calledrenderBlockagain, which resolved the same owner a second time and
allocated a closure per render. It now enters the owner in place and restores the
caller's owner when the render returns, throws, or suspends. The in-place frame
is installed with the signal document, so apps that never enable signals do not
retain it. A host carrier installed withinstallSignalOwnerEnvironment, or a
root given asignalOwnerwithout the signal document, still runs each Block
inside its own callback frame. A scope's resolved owner is also kept on the scope
instead of in aWeakMap, so each lookup is a field read and live Blocks no
longer occupy a weak table.On the new
app-frame-hydrationbenchmark (about 1,650 Blocks, 4× CPU throttle,
Chromium),renderBlockentries fall from two to one per render, production calls
fall by 4.0%, and hydration allocates 161 KB less. Paired with the previous
runtime, cold hydration is 1.8% faster and warm hydration 3.9% faster.247dffe: Coalesce native component renders from ready query and asynchronous derived signal results. Preserve every source publication while pacing stream pulls and ready results under a shared host budget, retaining cancellation, event urgency, and server backpressure.
3a94771: Run
useDeferredValue's deferred render in a later task, so the browser can paint the urgent commit and handle input first.The deferred render was queued on a microtask, which ran in the same microtask
checkpoint as the urgent commit that returned the previous value. The browser
could therefore neither paint the keystroke nor deliver the next one until the
slow deferred render had also finished, souseDeferredValuegave no
responsiveness benefit (#1864). The swap now runs in a host task posted after the
urgent commit, throughscheduler.postTaskwhere available, thenMessageChannel, thensetTimeout. Urgent updates that arrive before that task
runs only change the value it renders, so a fast typist's skipped values are
never rendered deferred.There is still no time-slicing: once the deferred render starts it runs to
completion, so a keystroke that arrives during it waits until it commits. Asyncact()waits for a pending deferred render as it does for other scheduled work.
Code that expected the deferred value after awaiting only microtasks now needs
to await a task, oract().79ca088: Stop development hydration from reporting an empty
styleattribute that an
early binding left behind.When an early binding (
adoptBindings) published a single style declaration as
unset before hydration, such asstyle={{ opacity: props.opacity }}with no
opacity, removing that declaration could leave an emptystyle=""on the
element. Development hydration then reported a mismatch where the server
rendered""and the client"". Hydration now treats the attribute's presence
as the early binding's doing too, and still reports an emptystyleattribute
that the binding did not leave.d1e308b: Stop running click handlers twice when a Solid-built widget is mounted inside an Octane root.
Octane stored delegated handlers on DOM nodes at
$$<type>($$click), the same property Solid's event delegation uses. Inside an Octane root, each library's dispatcher found and called the other's handlers. A click on a TanStack Query devtools row ran its toggle twice, so the details view never opened, and once Solid'sdocumentlistener was registered, every OctaneonClickon the page ran twice as well. Delegated handlers now live at$o<type>and$ocapture:<type>, which no other delegation reads. Compiled output keeps the same size, because each key is as long as before. Code that wroteel.$$clickby hand was relying on an undocumented internal and must writeel.$oclickinstead (#1882).6e6aa98: Render
@ifand@switcharms that call no hooks without a component Block of their own.The compiler now marks an arm hookless when it calls no hook,
useor context read, has no@try, and makes no call through a bare identifier while it renders (event handlers and refs don't count). On a client mount, such an arm renders in a lightweight scope inside its component's render. It skips the Block allocation and the per-Block render bookkeeping. An arm that holds hooks, hydrates, reads signals, or mounts into an already committed component still gets a Block, and a committed hookless arm swaps out through the same staged, rollback-safe path as any other arm.Paired measurements against the previous code in Chromium:
recursive-context: cold first mount about 3–7% faster; a partial unmount and remount about 10% faster.
spa-navigation: navigations that mount a 1,024-leaf route about 17% faster.
DOM output, effect and ref order, keyed moves, Suspense, transitions, Activity and hydration are unchanged.
5e922d9: Compile JSX written inside a host element's attribute, such as an event handler
that callsroot.render(<Toast label="saved" />)ortitle={String(<Badge />)}.
The compiler used to leave that JSX unlowered, so the emitted module failed to
parse. It now builds the same element it would anywhere else, on the client and
the server, including when a handler is a setupconstthe compiler installs
at mount and when a statement-bodied handler moves to module scope.8fa3398: Activate independent
<Hydrate>islands that become ready together one per task.Each island activation hydrates its widget in one synchronous block. Islands that
became ready together activated back to back in one microtask checkpoint, so the
browser could not deliver input or paint until every one of them had finished.
This happened with repeated instances of one widget, which share a module, with
islands released by one stylesheetloadorIntersectionObservercallback, and
with a back/forward-cache restore, which resumes every unfinished island at once.Now the first island to become ready in a task still activates at once, and each
one after it activates in a later task of its own, fromscheduler.postTask,MessageChannel, orsetTimeout. An island holding captured interactions takes
the next turn ahead of the others. An island waiting for its turn keeps capturing
input for its replay, and pausing or disposing it still cancels the activation.
Custom hosts that callregisterIndependentHydrationIslandget the same pacing.3557100: Type-check
<input ref={a} {...props} />in.tsrxwhenpropshas an index
signature, such asRecord<string, unknown>, as TSX does. Type checking
composes the explicit ref with the spread'sref, and arefthat only the
index signature admitted was read asunknownand rejected. It now counts as
absent; a declaredrefin the spread is still checked. Runtime output does not
change.1fb0ecd: Reorder keyed lists as fast as before 0.10.1 when every row moves.
0.10.1 made keyed swaps and removals cheaper by recording only the rows they
relink, so an abandoned render can put the list back. A reorder takes that record
before it knows how many rows it will move. When it then moved every row, as a
rotation or a move of a group of rows to the other end does, turning the record
into a full copy cost two extra Maps per reorder and, for a list that had been
reordered before, aSetof every row. The record now gets its links at the
first relink, so a record that has seen no relink is copied directly, at the
cost a full copy had before 0.10.1. It is also kept in its own undo entry rather
than in a separate per-render Map.In the js-framework reorder benchmark (1,000 rows, octane-tsrx, Chromium, paired
samples), rotating the rows and moving 3 to 8 rows to the end are 22–27% faster,
within 5% of the runtime before 0.10.1. Swaps, inserts and removals keep their
0.10.1 gains. The production framework bundle is about 110 bytes smaller.0d95b26: Write an unset or string
styledirectly in signal-enabled modules instead of
allocating a native style Block for it.A module that imports
octane/signalscompiles every non-literal style, such
as a forwardedstyle={props.style}, to a native style binding, because the
value might carry signal handles. The binding allocated and rendered its own
Block even when the value wasundefined,nullor a string, which has
nothing to read. A production audit of a signed-out home page found 273 such
Blocks in one hydration, about 8 ms at 4x CPU throttling. These values are now
written like an ordinary style, during client renders and when hydration takes
over an early binding's host. The Block is allocated only once a structured
value arrives, and then stays, so a later unset style releases its signal
subscriptions transactionally.Also fixes hydration of a binding view whose early binding claimed its style:
a scalar style literal such asstyle={{ opacity: props.opacity }}in a
signal-enabled module made hydration throw, and the host was rebuilt on the
client.a3dcbd9: Depend on
alien-signals3.2.1 so the workspace and@octanejs/alien-signals
resolve a single core version. Octane imports onlyalien-signals/system,
which is unchanged from 3.2.0.c0192ec: Pace buffered streamed RPC and optional renderer-response readers with the shared host budget. Preserve ordered delivery and backpressure, and recheck cancellation after waits so a retired RPC pull cannot publish a stale buffered value.
Observe server result rejections when the producer is created so a paced consumer can delay its first read without an unhandled rejection. The stream still delivers the sanitized error frame.
6df3359: Closing a portal no longer allocates a property dictionary for each of its two comment markers. Teardown used
deleteto remove the markers' event-range fields, and that switches a DOM wrapper into dictionary mode. It now clears the fields instead. Event routing is unchanged.On the
portal-swarmbenchmark, opening and closing all 600 tooltips allocates 4.6% less (3,101 KB to 2,958 KB), andopen_close_cycleruns 1.5–3% faster (paired, A/A-controlled).d30025a: Keep a committed portal's events working after a discarded render created another portal in the same target.
Portals that share a target also share its delegated event listeners, which detach when the last portal releases them. A root render can create a portal and then be discarded, for example when a sibling suspends with no Suspense boundary. In that case both the new portal's creation undo and its owner's teardown released the portal, so the target lost its listeners while a committed portal still used them, and that portal's handlers (a button's
onClick, say) stopped firing. This affected compiledcreatePortalhost children, portals returned as values, and portals that the same render created and removed again. Each portal now releases the target once.78750a0: Stop journaling the creation and renders of a Block whose parent the same root
render created.Every scheduled root render keeps an undo journal so it can roll back if it
suspends. It recorded a creation entry and a render entry for every Block it
created, including Blocks nested inside a parent it had also created, which a
rollback discards together with that parent. Those Blocks are no longer
recorded, including Blocks below a hookless child (their render entries are
kept when the hookless child is their direct parent). After replaying the journal,
a rollback now unmounts every Block the render created that is still live. That
covers a Block its parent never linked, such as a portal whose content suspended
before its slot was registered. A boundary holding a transition keeps recording
everything inside its own window. During a rollback, the teardown callbacks of
these never-committed nested scopes, such asnativeLocalHookdisposers, now
run parent first, as an ordinary unmount does, instead of in reverse creation
order.A first mount of the 2,047-component
recursive-contexttree allocates 8% less
(4,028 KB to 3,689 KB per mount), and a coldportal-swarmmount is 13% faster.
Opening and closing portals under already mounted components is unchanged.f183be7: Journal a structural slot change with one fewer Map, closure and log entry. Before a root render restructures an
@if,@switch, component or child slot, it records the slot so that a suspension can roll it back. That record now marks the slot's snapshot instead of keeping a second Map with an undo closure to clear it. Rollback is unchanged.On the
portal-swarmbenchmark, opening and closing all 600 tooltips allocates 6.6% less (3,111 KB to 2,906 KB), andopen_close_cycleruns 1–2% faster (paired, A/A-controlled). Bundles that include the root journal lose 16–22 raw bytes.0f3abca: Ship the production text of argument-free errors once per application instead
of once per module.The published package gave each module whose errors take no arguments its own
copy of the "Minified Octane error" formatter, about 190 bytes each. A bundle
carried one per such module, and a code-split application one in every chunk
that included any of them. Those modules now import one shared formatter, so a
bundle holds that text at most twice: once for argument-free errors and once in
the generic formatter that encodes arguments. Error messages are unchanged.A hydrating signals application built from the published package drops from 9
copies to 2 (1.4 KB raw). Split across 19 chunks, it drops from 22 copies in 10
chunks to 2 copies in one chunk (3.7 KB raw, 1.1 KB gzip).cbc1e8e: Mount component trees faster by keeping rarely used per-component state out of every Block.
Every rendered component, list row and control-flow arm is backed by a Block. Fourteen of its fields served only a few features: Suspense,
@tryand<Activity>boundaries,use()thenables, fetch-tree warming, the loop guard for updates scheduled from effects,useEffectEvent, and<ViewTransition>. Those fields now live on a small record that a Block allocates the first time one of those features touches it. Ordinary Blocks drop from 79 fields to 66 and construct faster.Paired cold first mounts before and after this change, in Chromium (40 fresh pages each):
memo-wall (8,006 Blocks): about 4% faster.
portal-swarm (1,205 Blocks): about 2% faster.
recursive-context (4,102 Blocks): about 1.5–3% faster.
Behavior is unchanged.
1048eab: Hydrate an empty
{x as string}text hole that is the sole content of a fragment, directive arm, list item, or component children without a recoverable mismatch. The server now emits the empty-text stand-in for these positions, as it already did for text holes with siblings, so hydration adopts the server DOM instead of regenerating it.188d60b: Keep the transition and hidden-Activity graphs out of applications that only
use Suspense.A
@tryboundary with a@pendingarm installed the whole transition driver
and the hidden-<Activity>re-render, so every Suspense application shipped
transition attempts, root holds, held-update replay and Activity re-hiding even
when it never calledstartTransitionor rendered<Activity>. Suspense now
installs only the off-screen swap it uses for urgent branch replacement.startTransitioninstalls the transition members, and<Activity>installs its
own re-render when it first hides. Behavior is unchanged.A server-rendered frame that hydrates native signal reads, a keyed list, a
@tryboundary, a portal and a behavior island drops 9.6 KB raw and 2.6 KB gzip
of production JavaScript, and a Suspense application without transitions drops
9.7 KB raw and 2.7 KB gzip.b4a61e7: Render a pending cue at urgent priority, so a cue in an ancestor commits before the transition it announces.
A
useTransitionoruseActionStateraisingisPending, or auseOptimisticvalue,
rendered at transition priority in the click's microtask flush. When that render
reached a descendant whose state the transition sets, the descendant rendered its
new state there too, so the browser could not paint the cue until the whole
transition render had finished (#1864). The cue now renders at urgent priority, as
React renders it in an urgent lane. Descendants show their previous state and
render the transition in its later host task.Because the cue is urgent, a component that suspends while rendering it shows its
boundary's fallback, as in React. A commit of cues alone, such as an async Action's
before its first update, no longer starts a View Transition; transition work that
commits with them keeps its capture.90a57d8: Commit
isPendingwith the previous state when the same component holds the transition's update, and render that update in the transition's task.In the canonical
useTransitionpattern, one component holds bothisPendingand
the state its transition sets. That component rendered the pending cue and the
transition together in one microtask flush, so a click paid for the whole
transition render before the browser could paint "pending" (#1864). The cue now
renders without the transition's own updates and shows their previous values. The
component renders those updates in the transition's later host task, as React
rendersisPendingin an urgent lane before the transition lane. The same holds
for auseOptimisticvalue set in that component, and for a cue that arrives
while the component's transition is already waiting. Such a cue renders outside a
View Transition and leaves the transition'saddTransitionTypetypes to the
transition's own render.An urgent update to a component whose transition is waiting still renders the
transition with it, andflushSyncandact()still drain both priorities.47e030e: Drop
useTransition'sisPendingin the same render that commits the transition's updates.React's transition lane carries
setPending(false)with the transition's own
updates, soisPendingfalls in the commit that shows them. Octane published the
falling edge from a follow-up after the transition's task flush, which cost a second
render and commit in that task: a component holding bothisPendingand the
transition's state committed"b pending"and then"b idle", and a separate
button'sidlecommitted after the panel its transition updated (#1864). The
falling edge now renders with the transition. If a render in that pass suspends and
holds the transition,isPendingstays true and noidlecommits. AuseOptimisticvalue that a synchronous transition showed reverts in that same
commit.The falling edge is transition work, so it now waits for the transition's task when
an urgent update takes over the transition's components, or when the transition
updates nothing. WhenflushSyncoract()drains a cue together with the
transition it announces, the cue still commits first, and its falling edge follows
in a later task.d788d32: Render transition-priority work in a later task, so a backlog of Action results or transitions commits once instead of once per microtask.
Transition renders were flushed on a microtask, like urgent ones. Every ready
continuation therefore paid for its own render and commit inside one microtask
checkpoint, and the browser could neither paint nor deliver input until the last
one had finished (#1864). A hundred synchronoususeActionStatedispatches
committed 101 times where React 19 commits twice. Transition renders scheduled
outside a render now wait for a host task of their own (scheduler.postTask,
thenMessageChannel, thensetTimeout), as React renders a transition lane in
a Scheduler task. That covers updates insidestartTransition, Action results,
transitions started after anawait, and, while an async Action is pending,
updates made outside a delegated event,flushSync, or a commit callback.The pending cue still commits first:
useTransitionanduseActionState
raisingisPending, anduseOptimisticshowing a value, keep the microtask
flush. An urgent update to a component whose transition is waiting renders it
at once, andflushSyncandact()drain both priorities. Code that expected a
transition's commit after awaiting only microtasks now needs to await a task, oract().e2044cb: Adopt DOM bindings on a page that the browser has translated. Text that Chrome's Translate has wrapped in
<font>elements no longer throws error #318, #321 or #286. This covers fixed views' text leaves and structural programs' static text and text holes. Adoption leaves the translated text in place, and bound text is replaced with plain text when its value next changes. Text translated after adoption also updates again: before, those writes were silently dropped. Addtranslate="no"to an element to keep its text untranslated.a7ae419: Keep updating an element's only text child after the browser translates the
page.Chrome's page translator replaces each Text node with nested
<font>wrappers
holding the translation and detaches the original node. Octane kept writing new
values into that detached node, so<span>{message}</span>and<span>{message as string}</span>went on showing the stale translation. As
React'ssetTextContentdoes, the next update now replaces what the translator
put in the element with the new text. Switching that child to an element or to
nothing also clears the translation. Text that sits beside other children keeps
React's behavior: a translated sibling Text node is not rewritten.21f911d: Update the shared TSRX compiler dependencies to
@tsrx/core0.5.5 and@tsrx/oxc0.20.0.octanenow declares@tsrx/oxc@0.20.0as its optional
compiler parser peer, so a project that compiles Octane in Node upgrades its@tsrx/oxcinstall from 0.16.0 to 0.20.0.The parser now gives template markup TSX's exact tree: the indentation between
children is its own whitespace text, and each@case/@defaultarm is one block.
Octane adopts that tree at the parser boundary, so templates keep compiling to the
same static templates, component slots, and fast paths. Indentation between
children also no longer reaches a component as empty-string children in
value-position JSX, soChildren.countandChildren.onlymatch React.The TSRX language changes in this release apply to
.tsrxfiles:A
//preceded by whitespace, at the start of a line, or right after a tag or
block starts a comment in template children;https://example.comanda//b
stay text.A static
<script>body cannot contain</scriptin any letter case, since
HTML ends the element there; write<\/script.A dynamic tag,
<{expr}>, must be an identifier, a member access, or a string
literal; compute any other expression into a local first.Every compile error has a TypeScript (
TS…) or TSRX (TSRX…) code.
JSX text and attribute strings are decoded once, by the parser. Both parsers
now give theirvaluedecoded, so Octane no longer decodes it again: the
browser compiler, and a Node compile that fell back to@tsrx/core's parser,
rendered&lt;as<instead of<. Decoding follows JSX, as in
React: numeric references and the XHTML named references are decoded, and a
later HTML name such as✓stays text. JSX's whitespace rule applies to
the decoded text, as Babel applies it. Layout checks read the text as written,
so text that is onlystill renders.Enum member initializers are now visible to the client-only server check and to
universal renderers'forbiddenGlobalsvalidation, so a client-only binding or a
forbidden global used in one is reported, and a member name used by a later
initializer is not mistaken for a global.3557100: Report a file that will not parse with the same error from the Node compiler as
from the browser compiler, by moving the Node parser to@tsrx/oxc0.20.0.A syntax error is a
SyntaxErrorwhose message ends with its position, as inUnexpected token (2:10). Itslocis that position,{ line, column }with a
zero-based column, andposis its offset. Before, the message had no
position andlocwas a{ start, end }range, which Vite discards, so Vite
named the file without a line and column.A template diagnostic, such as two outputs in one code block or a redeclared
binding, is now a plainErrorwith its code and alocrange, not aSyntaxError.Messages use
@tsrx/core's wording. An unclosed tag reportsUnclosed tag '<div>'. Expected '</div>' before end of template.where the
closing tag was expected, instead ofunterminated JSX element starting at byte Nat the opening tag. A directive written without parentheses, such as@if x { … }, reportsUnexpected keyword 'if'.
A
.tsrxmodule with a generic call signature in an interface, such as<T>(props: Props<T>): Element, now parses natively instead of falling back to@tsrx/core's parser. Its compiled output does not change.3557100: Let
octane/compiler/typescript's declarations type-check in a project whosetypescriptis TypeScript 7.validateNativeSignalNamesnamedimport('typescript').ProgramandSourceFile, which TypeScript 7's package
root does not export; it now declares the classic Program it needs
structurally, and still accepts a TypeScript 5.9 or 6ProgramandSourceFile.3557100: Type-check
@tryin.tsrxfiles for universal renderers such as@octanejs/ink. Type checking renders@tryas a<Suspense>(for@pending) and a<TsrxErrorBoundary>(for@catch), imported fromoctane,
whose DOM component types returnvoidandunknown. A universal renderer's
JSX accepts only components that return aUniversalRenderable, so TypeScript
rejected the boundaries in every such file. Type checking now imports
renderer-neutral stand-ins from the new type-onlyoctane/tsrx-boundary
subpath. Runtime output does not change.1e74902: Stop shipping inert profiling code from
vite devwhen profiling is off. Vite's dev server leavesdefineentries as runtime globals, so Octane's served runtime kept every__OCTANE_PROFILE_ENABLED__guard as a global read on its hot paths and still loadedprofiling.tsanddevtools-hook.ts. The Vite plugin now folds those guards out of Octane's own runtime modules in dev and gives Vite's dependency optimizer the same constant, so a pre-bundledoctanedrops them too. Profiling-on dev servers and production builds are unchanged.7e6bda5: Stop parsing modules that the Vite plugin and compiler only pass through.
Nitro, as used by TanStack Start, rebundles the SSR build's own chunks from
node_modules/.nitro, and Octane's Vite plugin ran over each one again. Before
checking whether the compiler would read anything from a module, the plugin's
preflight parsed every module in full. The compiler then attributed these
chunks to the application'spackage.jsonby walking up pastnode_modules.
Code samples inside the chunks' strings matched the hook-import text gate, so
the compiler parsed them a second time to hook-slot them, and the result was
unchanged. The authored parser is slow on large modules with few comments, so
the website's 5 MB docs chunk cost about 90 s and its 1.5 MB benchmarks chunk
about 80 s.A file inside a
node_modulesdirectory that has no manifest of its own,
such as a.vite,.nitro, or.cachebuild cache, no longer belongs to
the application that installed it. Node's package scope lookup draws the
same boundary.Preflight parses a module only when its transform can read a fact that needs
the AST. Facts that need it are compilation, hook slotting, and a virtual
barrel's descriptor re-exports. Every other module skips the parse.The static requests for a plain JavaScript module's server client-only check
now come from the module lexer instead of a parse. Module syntax inside
strings and comments no longer counts as a request.
In a local production build of the website, the Nitro phase drops from 2 m 50 s
to 0.5 s. Every emitted file keeps its content hash.43b60d9: Keep an early binding's whole-
stylepublication through hydration, as
single-declaration publications already were.When an early binding (
adoptBindings) owned a whole style, such as a
forwardedstyle={props.style}, and published a newer value before hydration,
development reported that value as a hydration mismatch. Once the binding was
released, a render with the hydration-time style did not write it, because
hydration had cached the skipped write as applied, so the early value stayed on
screen. The binding now publishes its style for hydration. Hydration keeps it
without a report, and the next render writes its style even when unchanged.
- 0.10.x
0.10.2
7 Oct 202662e6448: Fix Strong mode rejecting effect setup that reads or writes a ref's
current
or calls a state getter. Neither is an effect dependency: a ref holds a value
the effect uses without re-running, and a getter reads the latest state without
subscribing. Strong mode reported both asOCTANE_STRONG_EFFECT_HIDDEN_DEPENDENCY,
so previous-value refs, ref counters, aMapof callback-ref elements, auseLazyRefstore, andgetCount()failed to compile inuseEffect,useLayoutEffect, anduseInsertionEffectsetup. They now compile as they do
in React. The diagnostic still reports a reassigned module variable read in
effect setup, and render-time ref reads and writes and state getter calls stay
errors.
0.10.0
6 Oct 2026- removed
32e4f3f: Remove the server range that hydration left behind when a stale node, such as one a browser extension inserted, stood before a component's range in an element. The client rebuilt the component but kept the server's copy too, so the component's content appeared twice.
e233e4a: Stop shipping the signal binding runtime in apps that never pass signal
handles through ordinary props.Since async signals landed, the compiler bound every opaque text, attribute,
control, and textarea hole, such as{row.label as string}, as a potential
signal handle in every module. Every app therefore carried the binding runtime
and stamped signal identities on every component. Now such a hole binds a
handle only in a module with an import fromoctane/signals; a type-onlyimport type { SignalHandle } from 'octane/signals'is enough and does not
enable native reads. Modules using DOM bindings keep binding handles, and$-named expressions such as{props.label$}still bind everywhere.This removes 1.7 to 3.3 kB gzip of runtime from the benchmark applications
(rows 38,991 → 37,307 bytes, TodoMVC 44,217 → 41,076, chat 43,638 → 40,347).An untyped component that renders handles from a lazily loaded signals engine
can either type its props or set the newopaqueSignalHandlescompiler option
(also accepted byoctane()and@octanejs/vite-plugin) to keep the previous
behavior. In development, a handle that reaches a plain text hole logs an
error naming these fixes instead of silently rendering[object Object].Hydration through the ordinary writers now matches the signal-aware path it
replaces for these holes: a sibling or whole-output{expr}hole whose first
client value isundefineddiscards the server content it cannot adopt, and an
attribute whose first client value isundefinedremoves the server attribute.
Minor Changes
91079dd: Handle hydration mismatches the way React 19 does.
Fallback: a structural or text mismatch is no longer repaired in place. The nearest Suspense boundary,
@tryboundary,<Hydrate>island, or the root discards its server DOM, renders on the client, and reports once throughonRecoverableError.Attributes and raw HTML: attributes and
dangerouslySetInnerHTMLare never patched. Production compares nothing, and development warns.Root: a root keeps showing its server DOM until its client render commits. Its
<Hydrate independent>widgets survive the fallback with their DOM and state.
Apps that only call
createRootno longer ship any hydration code. In production,setHTML's<script>normalization is no longer needed and is gone (#1785).e233e4a: Stop shipping the signal binding runtime in apps that never pass signal
handles through ordinary props.Since async signals landed, the compiler bound every opaque text, attribute,
control, and textarea hole, such as{row.label as string}, as a potential
signal handle in every module. Every app therefore carried the binding runtime
and stamped signal identities on every component. Now such a hole binds a
handle only in a module with an import fromoctane/signals; a type-onlyimport type { SignalHandle } from 'octane/signals'is enough and does not
enable native reads. Modules using DOM bindings keep binding handles, and$-named expressions such as{props.label$}still bind everywhere.This removes 1.7 to 3.3 kB gzip of runtime from the benchmark applications
(rows 38,991 → 37,307 bytes, TodoMVC 44,217 → 41,076, chat 43,638 → 40,347).An untyped component that renders handles from a lazily loaded signals engine
can either type its props or set the newopaqueSignalHandlescompiler option
(also accepted byoctane()and@octanejs/vite-plugin) to keep the previous
behavior. In development, a handle that reaches a plain text hole logs an
error naming these fixes instead of silently rendering[object Object].Hydration through the ordinary writers now matches the signal-aware path it
replaces for these holes: a sibling or whole-output{expr}hole whose first
client value isundefineddiscards the server content it cannot adopt, and an
attribute whose first client value isundefinedremoves the server attribute.
Patch Changes
ed57b1e: Stop shipping the Fragment ref implementation in applications that use
<Activity>without<Fragment ref>. Revealing an Activity now recognises its Fragment refs by their live registration instead of aninstanceofcheck, so theFragmentInstanceclass is bundled only when a Fragment ref exists.8aeb6c0: Resolve a component's
signal$,derived$andquery$handles to the component's own cells when the children it passes to another component read them, as.tsxchildren evaluated by the parent already did. Compiled.tsrxchildren previously got cells of their own in the component that rendered them, so<Card>{record$.get()}</Card>started a second request for the query, in the browser and on the server, and a derived read computed from signals the component never updated. The fix covers children rendered in a slot, forwarded through another component's children, or rendered by<Suspense>,<ViewTransition>,<ErrorBoundary>or a context provider, and keeps hydration resuming the server's request.6182678: Resume a
query$on hydration when the server reads it only from a function the declaring body created, such as a render prop another component renders. The server now streams that query under the component that declared it, as the component's own read would. Previously the browser loaded the query again, or threw "The streamed signal selection bootstrap is missing or incompatible" when the page streamed no other query. A handle passed as a value, such as a prop, is still read in the receiving component's own instance.3fb1bec: Run a
derived$computation again once the thenable it threw settles, instead
of leaving an async reader retrying the settled wait without end.A synchronous
derived$that throws a promise is pending until that promise
settles. Settling it woke the value's readers, but the computation never ran
again. An asyncderived$that awaited the value withread()then read the
same settled wait on every microtask, which froze the page. The computation now
runs again when the promise it threw settles, and readers that subscribe to the
value are notified. A computation that throws a promise that has already settled
stays pending until a signal it reads changes.c15aa5a: Stop shipping form-action submission and Fragment refs to applications whose element descriptors cannot use them. Any renderable hole, such as a bare
{value},{children}or JSX stored in a variable, reaches the generic child renderer. That renderer used to keep the form submit driver (and through it the transition engine) and theFragmentInstanceclass in every such bundle, about 6.4 kB gzip. These now ship only when something can need them: the publiccreateElement,createElementAtandcloneElementinstall both. Compiled JSX installs form actions when a host or dynamic tag has anaction/formActionprop or a spread, and Fragment refs when a Fragment or dynamic tag has arefor a spread.5cad0fa: Preserve directory-upload file paths in early form-submission snapshots while keeping accepted files detached and immutable.
5041f00: Retire an early host binding when hydration replaces a host that was moved
inside the root while its boundary was pending.Before, the boundary rendered a new host at the original site but left the
moved host's early binding subscribed, so later source notifications kept
writing to the moved element. Committing the replacement now runs that
binding's cleanup, as an accepted handoff would. The moved element keeps its
last published values. A resumed attempt that suspends again leaves the early
binding live until a replacement commits.f7980e0: Hydration no longer loops when the next server node after an early-bound host (
adoptBindings) is stale content. Hydration now removes that content, reports a recoverable error and keeps the adopted host, the same as it does when no early binding is installed. The lease is still refused when anything else changes a neighbor of the host.97f1b8e: Keep pending derived signal readers attached to the accepted computation when it is redeclared.
fb404c2: Hydrate a form whose controls are named after element methods, such as
<input name="getAttribute">.A form exposes its named controls as properties, and they shadow methods like
getAttributeandmatches. Hydration read the form's attributes through that
property and threwgetAttribute is not a function, so the boundary rebuilt the
form on the client and dropped a submission accepted before hydration.
Independent hydration's mutation observer failed the same way onmatches.
Both now call the element prototype's methods.9bacb23: Fragment
addEventListenernow followsAbortSignallikeEventTarget. Aborting the signal ends the registration, so the same callback can be added again. A signal that is already aborted registers nothing, and the options are copied when the listener is added.2f88235: Keep each custom hook call's state independent when the hook reads through a getter or proxy, including during hydration.
8aeb6c0: Resolve a component's
signal$,derived$andquery$handles to the component's own cells when the children it writes between<Hydrate>tags read them.<Hydrate>rendered those children in a frame of its own, in the browser and on the server, so<Hydrate when={load()}>{record$.get()}</Hydrate>started a second request for the query and a derived read computed from signals the component never updated. The fix covers inline boundaries (split={false}), the children the compiler splits into a module of their own, children forwarded into a boundary, deferred activation, and a permanently staticwhen={never()}boundary on the server.b353f54: Hydrating a form control now projects a value or checked state that the client renders differently from the server, as React 19 does. The client value becomes the control's reset baseline: the
valueattribute of an<input>, the content of a<textarea>, and thecheckedattribute of a checkbox or radio, whether controlled or set withdefaultChecked. An input or textarea the user has not edited shows the client value, so a controlled control no longer displays the server's value while its state holds another. A value the user typed before hydration is kept until the control's first commit or discrete event, and a checkbox keeps its live checked state. A hydration whose values match the server's still writes nothing to the DOM.72cd60b: Fall back when hydration reaches a
@tryorErrorBoundarythat the server
did not render at that position.Before, a boundary with no server range built fresh markers. When its try body
threw, the catch arm rendered beside the server nodes that stood there, so both
were visible and nothing was reported. Now the missing range is a mismatch, as
it is for an@ifor@switchbranch: the nearest Suspense boundary,@try
boundary,<Hydrate>island, or the root renders on the client, as in React 19.393b75a: Fix nested interaction hydration in same-origin iframe ShadowRoots when the runtime belongs to the parent window. Preserve server-rendered targets and replay the activating event once, including events dispatched on foreign text nodes.
f50c128: Re-run an inferred hook when the receiver of a one-level method call changes, even if the method is one own function shared by several objects.
useEffect(() => source.subscribe())now moves its subscription to a newsourcethat sharessubscribe, and a memo callingsource.read()recomputes. An own arrow function still tracks only itself, because it cannot readthis, soprops.onChange(...)with a stable arrow callback stays quiet when the props container is rebuilt. Any other own function, includingvi.fn()mocks, now tracks its receiver, as React Compiler does for every method call.ba46692: Keep queries in an unread derived branch lazy across renders. When the branch is first entered, it uses the latest captured selection and loader.
f69215f: Keep named derived and query producers reader-owned when they capture local
signals. The same function, when forwarded as a callback, continues to read the
declaring component's cell. This also preserves owner identity through SSR and
hydration.9c84293: Resolve a component's
signal$,derived$andquery$handles to the component's own cells when a nested@{ … }block reads them, as directive arms and keyed rows already do. The block previously got cells of its own, so it started a second request for each query and computed from signals the component never updated. The fix applies in the browser, on the server and during hydration, and covers a nested block in JSX a component returns. A nested block still owns, and retires, the declarations it makes itself.087cd77: Stop inferred hook dependencies, compiler-memoized
use()arguments, and server-rendered prop creations from reading values an optional chain skips.run?.(options.label)andvalue?.[options.key]no longer throw whenrunorvalueis missing andoptionsis undefined, and no longer call anoptions.labelgetter before the authored code would. When the receiver exists, the dependency still followsoptions.labelas an own data property.c3c0a3b: Abort a pending
query$request when its owner is replaced or removed while a
Suspense boundary or@tryis still pending.Before, a retried attempt kept the old owner's request alive until the boundary
resolved. This happened when a child under a pending boundary changed only itskey, when an@ifarm or component was swapped, or when a nested boundary
was removed while its outer boundary waited. A newer attempt that renders a
different component, key or arm in that place, or completes without it, now
retires the old owner and aborts its request. A place that attempt has not
reached yet keeps its request, and so does an owner that renders again with the
same identity.262926c: Memoize
use()arguments in plain.ts/.jshook modules with the same
dependency rules as.tsrxcomponents.A plain module's dependency array read values its
use()argument skips.use(load(run?.(options.label) ?? 'idle')),use(load(() => options.label))
anduse(load(ready ? options.label : 'idle'))each threw whenoptionswas
undefined, and atypeof window !== 'undefined'guard read a barewindow
during server rendering. Skipped reads now use the guarded descriptor probe,
deferred reads useoptions?.label, and typeof guards are replayed.9dafaef: Build files outside the Vite or Rspack root to the same bytes from any checkout location. Their compiler module ID is now relative to the root (
../packages/ui/src/Card.tsrx) instead of the absolute host path. Every component and signal site hash in those files includes that ID, so moving a monorepo, or building in a different CI directory, produced different bundles from identical source. Workspace packages linked from outside the application root, such as Octane bindings in a monorepo, were affected.35c0d1b: Reduce the Suspense retry reveal window from 300ms to 100ms so ready content
spends less time behind a visible fallback. The default hold for already-visible
transition content remains indefinite.21e0575: Stop component-local
derived$andquery$chains from re-rendering without end
while a value they read is pending or rejected (#1735, #1736, #1737).Every render declares a component's
derived$andquery$handles again, so a
declaration that read another localderived$orquery$captured a new handle
each time. Its captured values never compared equal, so every render ran its
computation again. When that result could never equal the committed one (a query
still pending, a rejected query's error, a new object), accepting it re-rendered
its readers, whose render declared it again.act()reported that the scheduler
did not stabilize, and in a browser the tab stopped responding while the query
stayed pending or after it was rejected.A captured handle now matches the accepted one when it resolves to the same cell.
It differs only when its key selects another cell or when the render presents a
redeclared definition of that cell, so a dependent still reads the definition its
render presents. A local computation that reads other local handles also no
longer runs again on renders that change none of its captured values.8b97478: Report a render loop driven by a component's own
derived$orquery$as a
"Maximum update depth exceeded" error instead of freezing the page.A
derived$orquery$declared in a component body that captures a value the
body creates on every render, such as an inline object, runs again on every
render. If its result never compares equal, such as a new object, accepting each
render notified its readers, which rendered and declared it again. Those renders
started a fresh update each time, so the nested-update limit never applied:act()reported that the scheduler did not stabilize, and outsideact()the
page stopped responding with no error. Renders that a commit schedules through a
signal notification now count toward the update that caused them, so the cycle
fails with the update depth error, in production too. In development the message
names the component and the likely cause.A redeclared result that equals the accepted one, the same error or a read still
waiting on the same pending dependencies, no longer notifies the cell's readers,
so a body that captures a new object while its query is pending or rejected now
settles instead of looping.ab9a043: Re-evaluate a redeclared derived signal whose dependency changes while a held transition stages it, instead of committing its stale ready value.
- removed
32e4f3f: Remove the server range that hydration left behind when a stale node, such as one a browser extension inserted, stood before a component's range in an element. The client rebuilt the component but kept the server's copy too, so the component's content appeared twice.
7a92ad2: Preserve hydration handoff for unbound host attributes in production Strong mode.
a2a0c41: A hydration that suspends no longer lets outside code change the site of an early-bound host (
adoptBindings). When the suspended attempt rolled back, it restored the stale server content it had removed beside the host, but the host's parent stayed marked as repaired. If other code then removed that content, the resumed attempt accepted the lease anyway and retired the early binding. Hydration now refuses the lease in that case and the early binding stays live. The refusal goes to the root'sonUncaughtErrorinstead of escaping as an unhandled error, because nohydrateRootcaller is left to catch it.e03350a: Stop shipping Suspense hydration in applications that hydrate without Suspense.
hydrateRootno longer bundles the Suspense boundary machinery (mountTry, catch switching, Activity and hidden-reveal paths) unless the application renders a Suspense or@tryboundary, which saves about 9 kB gzip on a minimal hydrating client.0d6ef67: Keep the
keyon a<Fragment key={…}>written in a.tsrxtemplate, so a new
key remounts its children as it does in React.The template compiler inlined a long-form Fragment's children and dropped its
key, both in a@{ … }body and in a directive arm such as@tryor@if.
Changing the key therefore kept the children's state, effects, and a caught@tryerror. A keyed Fragment now renders through the same keyed descriptor
boundary as a keyed host element, on the client, on the server, and during
hydration. This also covers a keyed Fragment with directive children in
returned JSX. Unkeyed Fragments still compile to their inlined children.c213e87: Fix controlled inputs losing user edits when an
onXxxCapturehandler for the same event writes a signal that the form reads. A signal write in a capture handler is meant to publish with the rest of the event's handlers. For an event the browser dispatched itself, such as a keystroke, a tap or a drag, it published in the microtask checkpoint between the root's capture and bubble listeners instead. The form then re-rendered its controlledvalueto the old state beforeonInputcould read the edit, so a range stayed at its old value and typed characters were dropped. Trusted events now keep the capture write until the bubble handlers have run, or, when a native listener stops propagation below the root, until the browser has finished dispatching the event. Script-dispatched events are unchanged.da6524e: Specialize production roots and component calls over
.tsxcomponents the way.tsrxcomponents already are. Vite's TypeScript transform reprints every.tsxmodule after Octane compiles it, so the Vite plugin's check that an imported component still has the compiled code it proved never passed for.tsx. Every.tsxapp therefore shipped the generic root and component paths, about 30 kB gzip more than the same app in.tsrx. The check now accepts a reprint that changes only positions, literal spellings and comments, and still falls back to the generic paths when a later transform changes the program.ace4737: Recover when server HTML loses a hydration range's closing marker instead of
failing the root.Some HTML minifiers and proxies strip comments, and a range whose
<!--]-->was
removed made hydration throwCannot read properties of null (reading 'nodeType')and empty the container. Hydration now renders the nearest Suspense
or<Hydrate>boundary around the damaged range, or else the root, on the
client and reports one recoverable error throughonRecoverableError. An
application@catchorErrorBoundarydoes not receive it.b77ab18: Route errors from universal catch fallback effects and cleanups to an enclosing boundary instead of repeatedly entering the same catch fallback.
1744e83: Refresh a memoized
use()argument when the receiver of an inherited method call changes.use(load(count.toFixed(1)))used to depend oncount.toFixed, which isNumber.prototype.toFixedfor every number, so it kept showing the firstcount. It now trackscount, and the same applies to plain TypeScript hooks and server prop creations. An own function, arrow or not, still tracks only itself, souse(props.load(id))keeps its request when the parent rebuildspropsaround the sameload.023aa7d: Production Vite builds no longer hang when components import and render each
other. Before compiling a production client module, the plugin loads each
imported component to check whether it can use a lighter child slot. Inside a
cycle (A.tsrxrendersB.tsrx, which rendersA.tsrx), each module's
transform waited for the other's, so the build never finished. That happened for
cycles of any length and for separate entry points into one cycle, even when
recursion was bounded at runtime.The plugin now tracks which proof loads are waiting on which modules, and it
skips a load that would close a cycle. Every import on that cycle keeps ordinary
component dispatch, so a ring of components compiles the same way whichever
member the build reaches first. Imports outside a cycle keep the specialization,
including imports of a component that sits on one, and acyclic graphs compile
exactly as before.82a5f31: Keep reader ownership for a
derived$orquery$producer called through a namespace import behind a type assertion, such as(Signals as typeof Signals).derived$(compute$). It now gets the same per-reader producer copy as a direct import.
- 0.9.x
0.9.1
4 Oct 2026ff77a8a: Preserve early form submissions when an already-aborted behavior registration is followed by a live replacement.
f852068:
collectDiagnosticsno longer prepares and analyzes a module a second time when Strong accepts it. It continues compiling from the tree that Strong analysis already checked, sooctane analyzecosts about the same ascompile()on Strong modules instead of up to 1.26 times as much. It reports the same diagnostics and errors as before.77504c1: Cancel a component's superseded
query$request when only a pending@tryarm reads it.A
query$declared in a component and read with.get()inside that component's@try/@pendingboundary kept its previous request live after the component
committed a new selection. The arm's suspended attempt discarded the new
declaration, so the old request'sAbortSignalstayed un-aborted until the
component unmounted. The component's commit now selects the new request and
aborts or closes the obsolete one, for promise and stream queries alike. An
asynchronousderived$read the same way now adopts the committed computation
for its next dependency-driven restart instead of keeping the previous render's
closure.
Patch Changes
ff77a8a: Preserve early form submissions when an already-aborted behavior registration is followed by a live replacement.
f852068:
collectDiagnosticsno longer prepares and analyzes a module a second time when Strong accepts it. It continues compiling from the tree that Strong analysis already checked, sooctane analyzecosts about the same ascompile()on Strong modules instead of up to 1.26 times as much. It reports the same diagnostics and errors as before.77504c1: Cancel a component's superseded
query$request when only a pending@tryarm reads it.A
query$declared in a component and read with.get()inside that component's@try/@pendingboundary kept its previous request live after the component
committed a new selection. The arm's suspended attempt discarded the new
declaration, so the old request'sAbortSignalstayed un-aborted until the
component unmounted. The component's commit now selects the new request and
aborts or closes the obsolete one, for promise and stream queries alike. An
asynchronousderived$read the same way now adopts the committed computation
for its next dependency-driven restart instead of keeping the previous render's
closure.252ad4f: Propagate Context updates through hookless lightweight component scopes below memo boundaries.
483cd31: Build a JSX
<ErrorBoundary>fallback from the committed render after a parent render rolls back.When a parent re-rendered an
<ErrorBoundary>with new props and a later sibling
then suspended the root, the root render rolled back and the screen kept the
committed props. The boundary still held the abandoned render's captured values, so
a descendant that threw afterwards got a fallback built from props that never
committed. It now gets the committed render's fallback, for an element or a(error, reset) => …render prop, after an urgent update or a suspendedstartTransition.4601dd3: A function a component or hook creates now uses the signal cells that body
declared, wherever the function runs. Previously a Retry callback passed to a
child button reset a cell in the child, so a failedquery$stayed in its@catcharm and its loader ran again for nothing. The same happened to a
callback a child calls while rendering, which started a second query, and to
code afterawait, which resolved a document-level cell.The compiler routes method calls such as
result$.reset()on a body'sconst
declarations, inside the functions that body creates, through the declaring
instance, arm or row. Once that owner retires, such a call throwsScopeDisposedError. A handle passed to a child as a prop still resolves in the
child's own instance, andsignal$,derived$,query$and Scope producer
closures keep their reader ownership.8b03ce3: Keep sibling
@if,@switch, and component slots in source order at the root
of a fragment body.A body made only of directives and component calls, such as a component's root
fragment, an@ifor@switcharm, a@trybody, or a@forrow, has no
template, so every slot in it inserted before the body's shared end marker. An@ifor@switchtaking an empty arm, or a hookless component rendering one,
keeps no DOM of its own. When it later rendered content, that content landed
after every later sibling:<>@if (a) {<span/>} @if (b) {<em/>}</>rendered<em>before<span>onceaturned on. A slot emptied while a later sibling
rendered could also throwNotFoundErrorafter that sibling unmounted.The compiler now marks each such slot that has a later sibling, and the slot
gets a comment of its own at its source position when a client render creates
it. Hydration is unchanged: a hydrating slot adopts the server's range.c16fe6d: A
function … @{ … }component that is not a top-level declaration now has its hooks slotted once. This coversmemo(function X() @{ … }), afunction F() @{ … }declared inside a component or helper, one held in an object or array, and, on the server, one passed as a component prop. The compiler used to rewrite these bodies twice. When such a body destructured the third member ofuseState,useReduceroruseLinkedState, the module failed to load withSyntaxError: Identifier '_$__useStateWithGetter' has already been declared, or theuseReducer/useLinkedStateequivalent. Bodies without a getter loaded, but their built-in hooks took a second, unused slot.f9fd9a5: Hydrate each
@ifand@switchinto its own server range when a component's first render updates its own state while it evaluates a slot's arguments. The replayed render no longer reports a mismatch and rebuilds the server markup.322162c: Let a
@trywait for its component's replay when that render updated its own state.A non-Strong component can update its own state while it evaluates a slot's
arguments, such as a child's props or an@ifcondition. That render then
replays before its slots settle, and every other slot waits for the replay. A@try, or an imported JSX<ErrorBoundary>, instead mounted during the
discarded pass. In a fragment body its earlier siblings then rendered after it.
Its body also rendered from state the replay discards: a throw there committed
the@catcharm, and a suspension showed@pendingor suspended the whole
render. Hydration adopted the boundary's server range out of order and rebuilt
the component. The boundary now waits for the replay, and renders once from the
settled state.8489303: Remove an
@ifor@switcharm's content when a deferred hydration retry
resumed a component inside it.A
<Hydrate>boundary whose captures change while it is pending builds new
content on the client. If an arm in that content suspends before inserting
anything, the next retry renders the suspended component first, then its
parents. The arm still treated the component's output as its siblings' content,
so closing the arm later, in that retry or after it, left the output in the
document. The retry now marks that output as the arm's own.01ec8d7: Keep a Suspense boundary pending, and on its committed inputs, when the root
render that revealed it is rolled back.A parent update can reveal a pending boundary and then roll back because a later
sibling suspends the root. The rollback restored the fallback on screen, but the
boundary still recorded its primary as visible, so@pendingstayed up for good
and no later update revealed it. The boundary's record now rolls back with the
render, so the next commit, or its own data resolving, reveals it as before.Rollback also restores the boundary's inputs. Previously, a boundary that
retried after the rollback rendered the abandoned props: for example its own
promise settled, a child suspended or threw, or a JSX<Suspense>or<ErrorBoundary>rebuilt its children, fallback or catch arm. It now renders
the props of the committed screen.4c7ae9b: Keep a replaced sibling's content when an
@ifor@switcharm that suspended
inside a Suspense primary is superseded.An arm that suspends before inserting anything owns no DOM. It used to remember
the node in front of it, which belongs to a sibling. A keyed component, keyed@forrow, or@if/@elsearm beside it can replace that node, or stage its
replacement right after it, before the arm renders again. A later update that
dropped the arm then swept the sibling's new content away. In@try { <Child key={id} /> @if (wait) { <Hold /> } }, rendering A, then B with a
pending promise, then A again left the boundary without a child (#1698). A retry
that resolved after such a replacement also bounded its content from a detached
node, so its content outlived the arm.A pending arm with no DOM now reads its bound from its insertion anchor when it
renders again. A retry that inserts content and then suspends again owns that
content, the same as a first mount.
0.9.0
4 Oct 2026c1a8faf: Load the
idle(),visible()andmedia()triggers for independentHydratewidgets only when a page has a widget that uses one. The independent island bootstrap used to import all three, so every islands page shipped them, even one whose widgets used onlyload()andinteraction(). The signal-chat islands-only route now loads 612 fewer bytes of gzipped JavaScript. A widget with one of these triggers loads the strategies the first time the bootstrap registers it, then installs its trigger. A custom host can passstrategies: independentHydrationStrategies, from the newoctane/hydration/independent-strategiesentry, tobootstrapIndependentHydrationorregisterIndependentHydrationIslandto install triggers during registration instead.00f8304: Add
useLayoutSnapshotto derive a renderable value from a committed DOM measurement, with an SSR initial value and configurable equality.LayoutSnapshotOptionsWithInitialtypes a reusable options object whose initial value keeps the snapshot defined, and Strong mode reports anasyncor generator measurement asOCTANE_STRONG_LAYOUT_SNAPSHOT_ASYNC.dfff293: Add
useLazyRef(factory), which initializes a mutable ref from a factory when its hook cell is created and keeps that ref on later renders.useRef(callback)still stores the callback without calling it. Strong mode checks the factory with the same rules as a lazyuseStateinitializer: it may read a clock, randomness, or browser state, but may not schedule work. Strong mode now also checks hook initializers, reducers, and updaters passed through a literal array spread, such asuseState(...[() => value]). On universal renderers, a ref escaped from an abandoned render now keeps the last value written to it, as a plain object would. It no longer reverts to its initial value, and it no longer reads or writes a different ref committed later in the same hook slot.
Minor Changes
c1a8faf: Load the
idle(),visible()andmedia()triggers for independentHydratewidgets only when a page has a widget that uses one. The independent island bootstrap used to import all three, so every islands page shipped them, even one whose widgets used onlyload()andinteraction(). The signal-chat islands-only route now loads 612 fewer bytes of gzipped JavaScript. A widget with one of these triggers loads the strategies the first time the bootstrap registers it, then installs its trigger. A custom host can passstrategies: independentHydrationStrategies, from the newoctane/hydration/independent-strategiesentry, tobootstrapIndependentHydrationorregisterIndependentHydrationIslandto install triggers during registration instead.00f8304: Add
useLayoutSnapshotto derive a renderable value from a committed DOM measurement, with an SSR initial value and configurable equality.LayoutSnapshotOptionsWithInitialtypes a reusable options object whose initial value keeps the snapshot defined, and Strong mode reports anasyncor generator measurement asOCTANE_STRONG_LAYOUT_SNAPSHOT_ASYNC.dfff293: Add
useLazyRef(factory), which initializes a mutable ref from a factory when its hook cell is created and keeps that ref on later renders.useRef(callback)still stores the callback without calling it. Strong mode checks the factory with the same rules as a lazyuseStateinitializer: it may read a clock, randomness, or browser state, but may not schedule work. Strong mode now also checks hook initializers, reducers, and updaters passed through a literal array spread, such asuseState(...[() => value]). On universal renderers, a ref escaped from an abandoned render now keeps the last value written to it, as a plain object would. It no longer reverts to its initial value, and it no longer reads or writes a different ref committed later in the same hook slot.6651f44: Add
collectDiagnostics(source, filename, options)tooctane/compiler. It returns every diagnostic in a module, including every Strong violation, wherecompile()throws the first error. Strong diagnostics now point at the dedicated replacements: React's lazy ref initialization (if (ref.current === null) ref.current = …or??=) namesuseLazyRef, and an effect that copies a DOM measurement into state namesuseLayoutSnapshot. Where the rewrite is mechanical, the suggestion carries sourceedits: the lazy ref idiom becomesuseLazyRef(() => …),useMemo(() => value, deps)becomesvalue, anduseCallback(fn, deps)becomesfn. A thrown Strong error now ends with a link to its entry at octanejs.dev/docs/strong-mode.6651f44: Strong mode now rejects a synchronous state update in a host element's callback ref with
OCTANE_STRONG_REF_STATE_UPDATE. Octane calls a callback ref while the element commits, before paint, so the check follows the effect setup rules. It covers inline and local functions, a state setter passed as the ref, and functions in aref={[...]}list. When the callback copies a DOM measurement into state, the error namesuseLayoutSnapshot. Updates the ref defers torequestAnimationFrame, a timer with a positive delay, or an observer or event listener stay legal, and a component'srefprop is not checked.octane explainand the MCP server'soctane_strong_explaindescribe the new code and its migration recipe.
Patch Changes
1aa7696: Update a surviving row of a value-position list without re-rendering the row itself when its new element names the component it already shows. Lists that
createElementbuilds, as.tsbindings do, hand back a fresh element for every row on every render. Each surviving row used to run its own render only to reach the memo comparison of its component. A row whose props compare equal now bails with that comparison alone, and a changed row updates its component directly. Hydration, signal-owned trees, and any other change of child take the ordinary row render. In the memo-wall benchmark, a one-row change in a 1,000-row list makes about half as many function calls.8e68622: Run a hook with an omitted dependency array on every render when its callback reads a local variable that the component assigns again after the hook call, or assigns from a nested function. The compiler read the variable where the hook is called to fill the inferred array, so the array held the earlier value. The hook did not re-run when only the later value changed, while the callback read that later value. React runs a hook with an omitted array on every render, and Octane now does the same in this case. A variable assigned before the call keeps its inferred dependency. Strong mode reports
OCTANE_STRONG_UNTRACKED_EFFECTinstead, asking for the assignment to finish before the hook.12c32ec: Islands-only pages no longer download the code that stages resources and derived values inside a
startTransitionAction, about 900 bytes gzip. Under theoctane-islandscondition it now ships with the renderer, alongside the signal Action frame. In the default build it ships only in bundles that create a resource or a derived value. Actions stage resource and derived reads the same way in both builds.1066cf6: Keep the signal Action frame and transition coordinator, about 4 KB gzip, out of islands-only pages. Only the renderer uses them, to hold back signal writes made inside a
startTransitionAction until it settles, so a page that loads signals without the renderer never runs them. Until now every bundle that loaded the signal graph also carried them.Octane's package imports now choose where they live. By default they stay with the signal graph. A bundler that resolves with the
octane-islandscondition bundles them with the renderer instead. Both placements stage Action writes the same way, including for signals imported after an Action awaited. The condition only decides which pages download the code.@octanejs/vite-pluginadds the condition to the production client build when everyRenderRouteuseshydrate: 'islands'. Apps that also have fully hydrated routes keep the default.00f8304: Run state updates from insertion and layout effects at urgent priority, as React does during a commit. Previously an update from a layout effect that ran while an async Action was pending joined that Action and stayed hidden until it settled, and one from a commit flushed inside
startTransitionbecame a transition.00cb8a5: A passive effect that advances state on each commit no longer throws "Maximum update depth exceeded" after about 50 steps when a layout effect,
useLayoutSnapshot, ref callback oruseSyncExternalStoresync publishes one converging update per commit.useLayoutSnapshotno longer reports such a cascade as a snapshot that did not converge. Octane counted those synchronous updates across the whole passive cascade. React resets the count whenever a commit leaves no synchronous work, and Octane now does the same, so each passive-driven commit starts a fresh budget.Passive effects that run ahead of a re-render join it, so such a cascade can spend the budget without ever settling. When that happens, Octane renders the pending synchronous updates once without the waiting passive effects. A cascade that settles continues after paint with a fresh budget, and development builds warn about the long passive cascade, as React does. Layout, ref-callback, store-sync and
flushSync-in-useEffectloops still throw.39006d3: Passive effects that run ahead of a
flushSyncrender, including the drains of a synchronousact()callback, now count their updates as passive work, as React does. Octane previously counted them against the synchronous budget. A passive cascade that renders fine in a normal flush therefore threw "Maximum update depth exceeded" after about 50 steps under synchronousact(). AflushSyncthat the passive effect calls itself still counts as synchronous, soflushSync-in-useEffectloops still throw. Callback refs anduseSyncExternalStorechecks in the commit that such aflushSyncstarts keep the usual limit.16d6090: Read a deferred JSX value's fields once each time a child slot classifies it. A component that passes JSX as props, as in
<Viewport layers={<><Chart /><Legend /></>} />, had each of those values classified again on every re-render of the receiving component, and each field read ran the value's accessor and resolved its record. Classification now reads the type once and passes it along, de-opt list keys are read once per child, and a component descriptor's key and invocation site are read only when a new instance mounts. A deferred record that read no context now returns from its resolver without inspecting the current scope. In the svg-dashboard benchmark this cuts accessor calls per tooltip commit from 88 to about 25, for the same slot visits. Rendering, keys, context updates, and signal instance identity are unchanged.949894d: Resolve template row and catch reset bindings in their own scope when inferring hook dependencies and checking Strong effects.
39cf382: Recognize refs attached through callbacks in Strong effect analysis while preserving checks for value refs and host listener cleanup.
425a32a: Make in-place updates of a keyed list of Three meshes faster. Updating 1,000 retained mesh positions now takes about 0.7–0.8× the time React Three Fiber does on the Three benchmark, down from parity. The per-item comparison and validation work now runs in small functions the JavaScript engine optimizes as hot code. A full garbage collection no longer discards the optimized code that reads frozen host batches, so the first commit after one stays fast. Behavior is unchanged.
02e6726: Avoid collisions between generated signal and hook dependency imports, and prevent local bindings from shadowing inferred method dependencies.
414e192: Render-heavy updates are faster. Every component render restored the warm-plan
stack on exit by storing its length, even though most renders register no plan
and leave the length unchanged. The restore now compares first. In paired
same-runner runs, a memo-wall single-row change through a value-position list
took about 13% less time, a context update through 1,000 memoized rows about 5%
less, and recursive-context updates 4–5% less.
- 0.8.x
0.8.0
3 Oct 2026- removed
fbf6e40: Remove the server content left after a component's content during hydration. When the server rendered more in a component's range than the client renders there, the client adopted the matching prefix and kept the rest on screen without a report. This happened for a component whose identity differs on the client, for a hookless component in a renderable hole, and for a component that returns less on the client, including one rendered as a list item. Hydration now discards the stale remainder and reports it once through
onRecoverableError, plus a located development warning. The nodes the client adopted keep their identity. To find where a multi-root template's roots end without parsing it in production, the compiler now passes the template's root count totemplate(). - removed
bc30a18: Remove the server markup left after a component's last call when that call adopted its server node in place. When the server rendered another component's markup in a component's range, and the client's content there is a sequence of calls without server ranges of their own, each call adopts its server node in place. Whatever the server rendered after the last call's nodes stayed on screen, and hydration reported nothing. Hydration now removes it and reports the mismatch once through
onRecoverableError, while the nodes the calls adopted keep their identity. This covers single roots and fragments, in lite and full component slots, in development and production builds. 53db436:
'use dom bindings'views can now drive whole islands without the renderer:
Minor Changes
53db436:
'use dom bindings'views can now drive whole islands without the renderer:Imported signal reads (
count$.get(),count$.latest(fallback)) in projections, branch tests, lists and setup or blockconstdeclarations subscribe instead of raising an activation diagnostic. A pending read keeps the last DOM until its source changes.@try/@pending/@catchcompile into binding programs, select their arm like the renderer, adopt whichever arm the server rendered, and claim a boundary that is still streaming.useLayoutEffect(callback,)anduseEffect(callback,)run once after a view activates and clean up when it leaves.An independent
<Hydrate>whose only child is a prop-less, zero-argument binding view imported from a.tsrxmodule activates through that view's program, so its chunk no longer loads the renderer. Other islands keep the renderer activator.A live binding over signals whose owner retires, as a document's signals do when the page is left, now stops quietly with its last DOM instead of throwing.
3e3749d: Let
interaction({ events })opt intopointermoveandpointercancel, so a deferred boundary woken by a press can replay whether that press moved, was released, or was cancelled by the browser before hydration finished.Both events only extend an interaction that another selected event already captured: they never start hydration or prefetch on their own, so selecting
pointermovedoes not make a boundary hydrate on hover. Octane registers their document listeners only after a boundary that selects one captures intent, so pages that do not opt in never listen for pointer movement. The defaultinteraction()events are unchanged.Capture no longer cancels the native default of a captured
pointeruporpointermove, matching how it already treatspointerdown.950ef0b: Development server renders now report the client work an islands-only shell actually renders. The Vite dev server renders a
hydrate: 'islands'route with the newshellWitnessrender option and warns once per site when the shell, outside its independent<Hydrate>islands, renders an event handler or function form action, a ref, an effect or store-subscription hook, a controlledvalueorcheckedthe user can edit, or a live signal-handle binding. None of these would run, because the shell's modules never load in the browser.The report follows the render rather than the source, so it also catches what the build's source check cannot follow: components passed by reference, local aliases, handlers and refs passed through spreads, and elements a plain helper creates with
createElement. It covers only the branches, rows, and streamed boundaries a request reaches. The route keeps serving either way, and the production build check is unchanged.renderToString,renderToReadableStream, and the other server renderers acceptshellWitnessin development. Production renders ignore it at no cost, and the production compiler output is unchanged.dc4800b: Strong mode now requires state updaters and reducers to be pure
(OCTANE_STRONG_IMPURE_UPDATER). Octane evaluates queued updaters and reducers
while their owner renders and can call them more than once: a transition
update is evaluated when it is staged and again when the transition renders,
and an urgent update made while a transition is held is rebased onto the held
value. AuseStateoruseLinkedStateupdater, auseReducerreducer, or auseOptimisticreducer that callsfetch, schedules a timer, microtask, or
promise callback, updates state, calls a state getter or Effect Event, touchesuseRef.current, reads a browser global or reassigned module variable, or
callsDate.now(),Math.random(),performance.now(), ornew Date()is a
compile error. Inline functions, local and same-module declarations, and
synchronous helpers are followed. Mutating the state an updater or reducer
receives reportsOCTANE_STRONG_SNAPSHOT_MUTATION. Do the work in the event
handler and pass the result in. Compatibility mode is unchanged.dc4800b: Strong mode now rejects mutating a state value outside render
(OCTANE_STRONG_SNAPSHOT_MUTATION): in event handlers, effects, cleanup,
timer and promise callbacks, and local helpers that receive the value. Passing
a mutated array back to its setter does not re-render, and copying only the
outer object leaves identity-based consumers stale and rewrites the value that
transitions anduseOptimisticrevert to. Pass a new value instead, such assetItems([...items, item]), or keep mutable objects inuseRef. Array
mutators are now also recognized on nested literal properties and on state
initialized lazily or throughuseReducer,MapandSetmutators on state
created withnew Map()ornew Set(), andObject.assign-style targets;
render-time mutations keepOCTANE_STRONG_RENDER_SNAPSHOT_MUTATION.
Compatibility mode is unchanged.dc4800b: Strong mode now rejects a deferred state update computed from the same state's
render snapshot (OCTANE_STRONG_STALE_STATE_UPDATE). After anawait, or in asetTimeout,setInterval,requestAnimationFrame,requestIdleCallback,queueMicrotask, or promise callback,setCount(count + 1)can overwrite an
update that happened in between. Locals computed from the snapshot before theawait, copied aliases, and closures are followed. Use the updater form,setCount((current) => current + 1), or read the latest value with the state
getter. Synchronous handlers such asonClick={() => setCount(count + 1)}
remain valid. Compatibility mode is unchanged.752028d: Strong mode now rejects a
useSyncExternalStoregetSnapshotorgetServerSnapshotthat provably returns a new object or array on every call
(OCTANE_STRONG_UNCACHED_STORE_SNAPSHOT), such as a literal, a spread copy,.map(),.filter(),Object.keys(), a standard constructor, or a
same-module function or local constant that allocates. Snapshots are compared
withObject.is, so such a component warns in development and then renders
until the update-depth limit throws. Return a value the store keeps, or read
each field with its ownuseSyncExternalStorecall. Compatibility mode is
unchanged.dc4800b: Strong mode now rejects write-only state (
OCTANE_STRONG_WRITE_ONLY_STATE): a
state tuple whose value is elided, unused, or read only to compute its own next
value, whose getter is unused, and whose setter is called or passed on. This is
the force-update pattern, includinguseReducer((x) => x + 1, 0)anduseState(0)[1]. It reads the external source during render and subscribes
afterwards, so a change in between is never rendered. Subscribe withuseSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)instead.
Compatibility mode is unchanged.7422815: Universal renderers now ship host-binding and template-program code only when they use it. A Three scene's production bundle is about 5.2 KB gzip smaller.
universalHostBinding()installs the subscription, flush, and binding-only transaction code on its first call, so roots in an app that never creates a binding no longer carry it. Binding behavior is unchanged.Template programs are now opt-in per driver. A driver's
templateProgramMount,templateProgramRuns, andcollapsedTemplateMountcapabilities take effect only when the driver also setstemplates: universalHostTemplates, a new export fromoctane/universalandoctane/universal/native. A driver that declares these capabilities without it mounts and updates those trees through ordinary host commands. The Lynx background driver passes it.
Patch Changes
- removed
fbf6e40: Remove the server content left after a component's content during hydration. When the server rendered more in a component's range than the client renders there, the client adopted the matching prefix and kept the rest on screen without a report. This happened for a component whose identity differs on the client, for a hookless component in a renderable hole, and for a component that returns less on the client, including one rendered as a list item. Hydration now discards the stale remainder and reports it once through
onRecoverableError, plus a located development warning. The nodes the client adopted keep their identity. To find where a multi-root template's roots end without parsing it in production, the compiler now passes the template's root count totemplate(). a50b846: When hydration adopts an element whose children are all component calls but
the server rendered other content inside it, the first call now reports the
server node it found in place of its range once, the element's server children
are discarded, and the components are built inside the adopted element. A
single-root component used to compare its template with the element itself,
reporting that element, keeping the stale server children, and appending its
output after them; a lite component removed the adopted element entirely. Later
sibling calls in the same element no longer discard the rebuilt content of an
earlier one, and an element the server left empty reports "nothing".9ef7385: Fix a crash when a pending
<Hydrate>boundary resumes a component that
suspended inside content its parent opened in a server-rendered row.A deferred boundary's retry resumes the component that suspended before it
renders the rest of the boundary. When a hookless sibling component followed
that component, for example a later row of the same list, the retry picked the
sibling instead and threwTypeError: Cannot read properties of undefined (reading 'clear')once the promise settled. The retry now resumes the
component that suspended.6ea582e: Hydrate a component call at a hole in an
@ifor@switcharm when the server rendered another arm with the same static roots, for a call that cannot render in place of one server node. The server node at the call's position is an element of the other arm, not the component's range. A keyed or dynamic call, or a component that renders several roots, a fragment, or nothing, left that element on screen. A call after one of the arm's adopted roots also deleted that root. A component with several roots in a development build lost the adopted node after the hole. The call now replaces exactly the server node at its position, the arm's adopted nodes keep their identity and keep updating, and hydration reports the mismatch once throughonRecoverableError.52c9d33: Discard the server content left after a hydrated
@ifor@switcharm that ends with a@trywhose body throws on the client. The boundary builds its catch arm and leaves the hydration cursor on its own close marker, so the server components the server's longer arm rendered after it stayed on screen with no report. Hydration now looks past that marker, removes them, and reports one structuralonRecoverableError.8f8349b: Compile a bare-left
@for (item of items)or@for ({ id } of items)header instead of crashing. Each row binds its own item, as aletheader does, and shadows any outer name, on the DOM client, the server, hydration, split<Hydrate>boundaries, and the universal and Valdi renderers. A header that would assign an existing target, such as@for (obj.x of items), now reports a located compile error.f355acd: Keep a binding island's server content and input while its client data is still loading at activation:
A
@tryin a'use dom bindings'view no longer replaces server-rendered content with@pendingwhen the client's own read is still pending. Like the renderer's resolved boundary, the server content stays and hydrates in place when the read settles; its handlers and mount-only effects start with that commit. After the first commit, a later pending read shows@pendingas before.An island whose binding view suspends outside
@tryon its first render no longer loses input. Clicks captured before activation, and input that reaches the island before its data arrives, now wait for the first commit and then replay in order. Input held by an island that is disposed before it commits is dropped.
3ffd8cc: A
'use dom bindings'view whose text leaf casts a signal handle tonumber({count$ as number}, or a handle passed through props) now server-renders the handle's text, soadoptBindingscan adopt its own SSR output instead of throwing a mismatched-topology error. The ordinary renderer hydrates the same output in place.9350268: A DOM binding program, such as an island view, no longer ends silently on any error named
ScopeDisposedError. It ends quietly and keeps its last DOM only when the owner of a source it observes retires. Observed sources are a tracked.get()read, a bound signal handle (concrete, declared, optimistic, or accepted by a transition), a whole-style or projected binding, and a bound control. A read of a different, already retired scope, or any error that only shares the name, is now reported throughreportError. Like any other failure of a committed program, it keeps the last DOM and later updates still apply.4212874: Keep the hydration range-marker validator out of apps that never call
hydrateRoot. A dynamic{x}text hole that may hold a signal handle
previously retained the binding-marker protocol in every client bundle, about
600 B gzip. Adopting a binding view's server text range is now a hydration
method, so client-only apps drop it. Hydration behavior is unchanged.f03d812:
'use dom bindings'views may now declare no props parameter. A view that reads only module-scope signal handles, such as an island's state, compiles foradoptBindings,mountBindingsand server rendering like a view with props. Views with more than one parameter are still rejected.3315f74: Keep hydration aligned after a
@try,<ErrorBoundary>,@for, or<Activity>. A@tryor<ErrorBoundary>left the hydration cursor inside its own server range, so a following component with several roots in a body without a template, such as<>@try {…}<Pair /></>, did not find its server range: it was rendered a second time beside the server's copy and reported a false mismatch, even when the server and client rendered the same props. Every boundary, list, and activity now steps past its server range as components and branches do. This also lets an@ifor@switcharm that ends with one of them find where its content ends, so elements or text that the server's longer arm rendered after it are removed and reported once instead of staying on screen without a report.f5896c6: Discard the server content that a hydrating
@ifor@switcharm leaves after its last component or nested branch. When the server rendered a longer arm, such as one with a trailing element after the same components, that trailing content used to stay on the page with nothing reported. Hydration now removes it, keeps every node the client adopted, and reports the mismatch once toonRecoverableError, with one development diagnostic at the branch.3770c1e: Discard the server elements and text that an
@ifor@switcharm leaves unclaimed during hydration. When the server rendered a longer arm, the client arm adopts the start of the server's range. Elements or text after the roots of the client arm's own template, or after the single root that a component in the arm adopts in place, stayed on screen with no report. Hydration now steps past those roots, removes the server content after them, and reports one structuralonRecoverableError, with a development diagnostic at the directive. Every compiled multi-root template now carries its root count, so production hydration finds that end without parsing the template, including for a branch in a helper passed as a render prop.3994e1e: Discard the server content an
@ifor@switcharm leaves unclaimed during hydration. When the server rendered a different arm, the client arm adopts the server's range from its start. Server components or ranges left after the client arm's content stayed on screen until the next arm swap. Hydration now removes them and reports one structuralonRecoverableError, with a development diagnostic at the directive. A dormant<Hydrate>boundary whose captures changed before activation discards them without a report.6f019c5: Hydrate an
@ifor@switchat a hole in an@if/@switcharm when the server rendered another arm with the same static roots. The hole's position then holds an element or text of the other arm, not the slot's range. The slot used to treat that node as the node after it: a branch with several roots adopted it and the arm's next static root as well, so a node was lost silently, and later updates left the server node on screen or duplicated content. An empty branch kept the server node, a branch after one of the arm's adopted roots deleted that root, and a single-root branch that did not match threwNotFoundErrorfromhydrateRoot. The slot now takes the place of exactly that server node. It adopts the node when its branch is that node, otherwise it rebuilds over the node or removes it and reports the mismatch once throughonRecoverableError. The arm's adopted nodes keep their identity and keep updating.6c50c70: Read a component's signal and query declarations from its own
@try,@pending,@catch,@ifand@switcharms and keyed@forrows without creating a second
cell. A query declared in setup and read again from one of those frames, directly
or through a localderived$, now starts one request on the client and resumes
the server's request during hydration, instead of starting another request per
arm or row. A component's own signal displayed in an arm also keeps its value
when that arm remounts.An arm or row still owns the declarations it evaluates itself and resets them
when it is removed, and a child component that receives a handle as a prop still
resolves it in its own instance.de31710: Cache a
constthat only another cached calculation reads. Inconst labels = formatRows(rows); const view = wrapRows(labels);with onlyviewin the template,labelsused to recompute on every render, soview's cache never hit andwrapRowsran on every render too. Client builds now cache every link of such a chain, keyed on the chain's own inputs, in@{}andreturn <jsx>components alike. That includes aconstread only by a Strong inline render expression. Declarations that are never cached (hook calls, live member calls,let, values only an event handler reads) still never are.Also fix a render crash: a cached calculation whose callback read a
constdeclared after it, as inconst reader = make(() => labels); const labels = formatRows(rows);, threwCannot access 'labels' before initializationbecause its dependency check readlabelsearly. Such a calculation is no longer cached.fbaf501: Release the remaining host ref array owners when a callback cleanup throws, including nested arrays, and preserve the first error for existing error delivery.
70ee6a3: When hydration finds other server content in a range where the client renders
several siblings, such as server text in a{hole}whose client value is a
component that renders two components, the development console now logs one
hydration mismatch diagnostic for that recovery instead of one per sibling.
Once a recovery reaches the end of the range, by discarding the server content
before it or by rebuilding a single-root clone over the last server node,
later sibling components, fragment clones, and single-root clones that find
that end build on the client without a second "the server rendered the end of
the parent block" diagnostic. A mismatch in a separate range still reports on
its own.ab2798e: Mount and update every component without allocating a closure context. The hydration path for a component inside a client-rebuilt subtree now calls a separate helper, so ordinary component calls no longer capture their arguments up front. Rendering behavior is unchanged.
95421bb: A
signal$,derived$, orquery$declared inside a custom hook now belongs to each call of that hook. CallinguseUser$(a)anduseUser$(b)in one component previously shared one cell, so the second call showed the first call's data and never ran its own loader. Each call now owns its cells, a call skipped by a condition keeps its own, and server and browser builds key them identically for SSR seeds and streamed results. Call sites are keyed in modules with a runtime signals import and for hooks named with$.9168bf6: Fix a runtime host element (from
createElement) duplicating its children when
hydration retries after suspending. When the server rendered other children than
the client's, the first attempt built the client's children inside the adopted
host; the retry then built a second copy beside them. The retry now replaces the
earlier attempt's children, and a root that retries restores the server's.
A child host element that replaces server content now callsonRecoverableError
once across retries, and in development warns once at the hole that renders the
runtime host.11a9da9: Adopt the server's nodes when a list item suspends while a deferred
<Hydrate>boundary hydrates. A component that returns JSX passes the boundary its children as descriptors, so a host's children there hydrate as a list. When the first item's first render suspended, the boundary's retry filled that list again from wherever the resumed render had left the cursor. It reported a hydration mismatch, rebuilt the item on the client, and duplicated server nodes such as the item's first root. The retry now adopts the list from its first server item, as a compiled@fordoes. An item whose render suspends inside a hydrating@ifor@switcharm the server rendered no range for also leaves the server's nodes as they were, so the retry adopts that arm instead of rebuilding it.92495b9: A
@catchparameter written as an object or array pattern, such as@catch ({ message }: Error, retry), now binds its names on the client. Previously the client error arm threw aReferenceErroror read a component local with the same name, while server rendering showed the error, so client render and hydration disagreed with SSR. Pattern defaults may read component locals, as in@catch ({ message = fallback }).bfed959: On an ordinary page exit,
installSignalDocumentLifecyclenow unmounts independent islands before it retires the document's signal owner. Island cleanups can read their document signals instead of hittingScopeDisposedError, and live islands are no longer notified of the retirement. The owner still retires if an island's unmount throws.c016c2d: Discard and report the server's
@if/@switcharm when the client's arm renders nothing during hydration. An@ifwith no client arm already discarded the server content and reported it as an empty branch, but a client arm with an empty body, such as@else { <></> }, left the server's other arm on screen with no report. Hydration now removes that content and reports one structuralonRecoverableError, with a development diagnostic at the directive. When the server arm starts with an element or text node rather than a range, it is still kept.21c3bfe: When hydration rebuilds an
@forthat the server rendered with no items because
the client has items, the development diagnostic says the server renderedan empty list (@empty)only when the server rendered that arm. A list without
an@emptyarm now reportsan empty list.320f9e3: Mount rows with event handlers without per-row weak-map bookkeeping. A module that can receive signal handles recorded each native handler's signal authority in two weak maps, so every mounted row paid two weak-map insertions even when no signals were in use, which made prepending 100 rows to a 1,000-row keyed list about 1.6 times slower. The authority now lives on the host beside its handler, and a scope's published-authority mark is a field on the scope. Updating a text hole with a plain value in such a module also writes the text directly instead of going through the signal binding path.
da9b1e4: Preserve the first script or stylesheet preload's options when equivalent calls are deduplicated, so a later preinit inherits the original integrity and connection metadata. Explicit preinit options continue to take precedence.
4ab33b6: The compiler now rejects
for…inand C-style@forheaders on every target.@for (const k in object)used to compile asfor…ofover the object on the
DOM client, server, and universal renderers, rendering its values or nothing
instead of its keys.@for (let i = 0; i < n; i++)crashed the DOM compiler
with an internalTypeError. Each now reports a diagnostic at the directive
that suggests thefor…ofspelling, such as@for (const name of Object.keys(object); key name).8e90048: Keep every root of a hydrating
@ifor@switcharm that suspends inside a deferred<Hydrate>boundary while the server rendered no range for it. When the arm's last root was static, after the component that suspended or after ause()in the arm itself, the boundary's retry still bounded the arm after its first roots. Hydration looked right, but switching to another case left the arm's later roots on screen, and a case change while the boundary was pending did the same. The arm now ends after every root its template adopted, both when it first suspends and when the boundary retries it.6bb20ac: Keep the server nodes that a
@switchor@ifarm adopted when the arm
suspends inside a deferred<Hydrate>boundary before it has a range of its
own.When the server rendered no range for a
@switchor@if, the client's arm
takes the place of the server node at that position and adopts it. If that arm
suspended inside a<Hydrate split={false}>boundary, the boundary's retry
published the arm's range after the sibling that followed it, so that sibling
reported a false mismatch and built a second copy of its server node. A case
change while the boundary was still pending removed the wrong server nodes, with
the same result for the next sibling. The arm now marks where its content starts
with a comment of its own and keeps track of how far that content reached. Its
retry finalizes the range from there, and another case replaces exactly that
content and renders on the client without reporting a mismatch. This works for
arms whose content is text, several roots, or a root the arm had not yet cloned.6959fa0: Rebuild a hydrated fragment that starts with a hole when the server rendered something else there, such as another
@ifarm. A leading component call, text hole, or nested block matches any server node, so hydration adopted the other arm's nodes as the fragment's roots: its static roots were never built, and a component in the hole took one of the server nodes as its position. Hydration now compares the first static element root after the leading holes with the server node it would adopt. On a mismatch it rebuilds the fragment on the client, as it already did for a fragment that starts with a static root, and reports oneonRecoverableError. The development warning names that static root and the server node found in its place. Matching fragments adopt their server nodes as before. AHYDRATION_RANGE_BOUNDARYpassthrough root, whose owner may adopt one range level off, keeps its existing behavior.105e0f0: Run a hook with an omitted dependency array on every render when its callback reads a binding declared after the hook call. That covers a later
const,let,classorvar, and theconstthat receives the hook's own result, such as auseCallbackthat calls itself. The compiler read the binding where the hook is called to fill the inferred array. Alet,constorclassthrewReferenceError: Cannot access … before initializationthere. Avarwas stillundefined, so the hook never re-ran when it changed. React runs a hook with an omitted array on every render, and Octane now does the same in this case. Strong mode reportsOCTANE_STRONG_UNTRACKED_EFFECTinstead, asking for the declaration to come before the hook.0d91b93: Give a component rendered inside an
@ifor@forarm of a hookless component
the same signal instance on the server and in the browser. A hookless
component's arms, rows and value children no longer lose that component's level
from their signal and query identity, and no longer gain a stray list-item
segment. Hydration now resumes the server's cells for them instead of starting
fresh ones. Hookless siblings also keep their own owners when hydration retries a
suspended@trybody.7698907: Adopt a server-rendered
@catcharm (or JSXErrorBoundaryfallback) during hydration when the client's try body throws again, instead of reporting a hydration mismatch and rebuilding it. This covers a boundary that is a component's only output and a try body that renders a host element before it throws. The server now marks a caught arm, so the client replays the try body without claiming the catch arm's DOM. A body that renders or waits on the client replaces the server's catch arm without a mismatch report. The replay also reads theuse()values of Suspense boundaries andHydrateislands the server completed inside the abandoned try body, including streamed ones, so a body with a nested boundary adopts the catch arm immediately instead of waiting for client data. When a boundary at the top level of a root rebuilds during hydration, the root's next sibling component now hydrates in place instead of being rebuilt with a mismatch report.3baa492: Hydrate a server-rendered
@catcharm when a template-less component'suse()rejected on the server. The component now reads its rejection seed, so the boundary adopts the server's catch arm and reports the caught error instead of discarding that arm, reporting a hydration mismatch, and staying suspended. A component that renders where the server rendered something else still reports the mismatch.1fa3853: Hydration no longer reports a mismatch for a
@trybody or<ErrorBoundary>child that throws to its catch arm after adopting server content. A component that cloned its element and then threw from a hole was compared against whatever the server rendered there, often the server's own catch arm, and logged a development mismatch warning and calledonRecoverableErrorin development and production. The catch arm replaces that content, so these reports are now dropped. A body that completes or suspends still reports its mismatches, and nested boundaries pass them to the enclosing boundary.62e76bf: Load the browser's own query selection when hydration finds a server result for a
different request. When a component's props or state select a query request
other than the one the server resolved under the same owner, hydration no longer
fails with "The presented query definition does not match". The query loads its
current selection as if the server had not seeded it, and adoption keeps the
server's nodes while reporting changed text as a recoverable hydration mismatch.a1f136c: Deferred hydration no longer lets a live interaction overtake one captured
before hydration. A<Hydrate>boundary or independent island replays its
captured events after its hydration commits, which can be a frame later. An
event that arrived in between reached the hydrated handlers immediately, so
clicking option A before hydration and option B during it ran B's handler first
and left A selected. The boundary now keeps capturing until its replay runs, so
handlers see input in the order the user produced it. An island whose content
is still suspended after activation also keeps those events instead of
dropping them on the inert server markup.39f3e97: Deferred hydration now replays captured interaction events with the original
event'stimeStamp. Each replay used to be a newly constructed event stamped
with the replay time, so the time between a capturedpointerdownandpointerup, or how long an input had been held, collapsed to the hydration
delay. A boundary nested inside another boundary reset the clock a second time.
Replays from<Hydrate>boundaries, nested boundaries, and independent islands
now keep the captured value and remain untrusted (isTrustedisfalse).11aebc7: Hydrate a single-root component whose body is an
@ifor@switch, called where the server rendered another branch's markup. Hydration renders such a call in place of the server node it finds there, but the branch inside the component bounded its content against that node. When the branch's root did not match the node, recovery rebuilt the root and removed the node, andhydrateRootthrewNotFoundErrorand left the container empty. This happened in development and production builds, for an element or component arm, including when the call was its host's last child or the last root of an arm's fragment. When the root matched, hydration adopted it outside the branch's range, so switching the branch to another arm left the server's node on screen next to the new arm. The call now renders before the node after the server's node, so the branch bounds exactly its own root. A mismatch is still reported once throughonRecoverableError.416882f: Hydrate the component after one that adopted a server element in place. When the server rendered a different
@if/@switcharm, a component that found that arm's element where its own server range belonged adopted it, but the next component adopted the same element again, and the server's range for the next component stayed on the page. When the first component's own holes left the cursor inside the element, the next component claimed the enclosing block's range instead, which discarded the arm. Hydration now continues after the adopted element. A returned single-root component that hydration rebuilt at the end of its parent's server range no longer takes that range's end marker as its own, so a later render that hides it removes it, and showing it again no longer throws.3ec43da: When a component call finds no server range of its own, because the server rendered another
@ifor@switcharm there, and hydration adopts the server markup in place, a component call inside that component's template now adopts the server node at its own position. Previously, when the server had rendered that inner call's markup inline, the inner call rendered against the first server node of the outer template instead: it reported a mismatch and replaced that node, which the outer component had already adopted.0b833bb: Adopt the server's text at a renderable hole in a fragment that a component adopts in place during hydration. When a component call finds no server range of its own, because the server rendered another arm or component inline there, its fragment template adopts those server nodes in place, and the server framed none of that markup's holes. A
{value}hole then took the server's text node as its own end marker and inserted a second copy before it, so the page showed the text twice. The hole now adopts that text, or the element an element value renders, and updates it in place. A{null}hole at the end of such a fragment no longer claims the server node after it as the fragment's content, so hydration removes that stale node and reports it once instead of keeping it on screen.11aebc7: Hydrate a component call that has no server range of its own when a component inside the callee's branch adopts the server node at the call. This happens when the server rendered another branch's markup there, and an arm of the callee's
@ifor@switchrenders a component whose root matches that node. Hydration rendered the inner component in place of the node, but the call itself did not record that it had adopted the node. The page looked right after hydration, but two things went wrong. Swapping the component that the call renders, for example through an imported binding, left the old component's content on screen next to the new component. When the call was the whole arm of an adopted branch, the server nodes after the adopted node in that arm were never removed. The call now adopts the node with the component inside it, in development and production builds. A swap replaces the call's content, and the leftover server nodes are removed and reported once throughonRecoverableError.- removed
bc30a18: Remove the server markup left after a component's last call when that call adopted its server node in place. When the server rendered another component's markup in a component's range, and the client's content there is a sequence of calls without server ranges of their own, each call adopts its server node in place. Whatever the server rendered after the last call's nodes stayed on screen, and hydration reported nothing. Hydration now removes it and reports the mismatch once through
onRecoverableError, while the nodes the calls adopted keep their identity. This covers single roots and fragments, in lite and full component slots, in development and production builds. b319c16: Production builds emit less code for inlined
useMemoanduseCallbackcalls. When the compiler supplied the memo's slot, the hook's name is no longer passed to the runtime, since only an authored slot can be missing. In plain.tsand.jsmodules, a dependency that is a never-reassigned local binding is now read directly instead of being copied to a temporary first. Across the repository's binding modules, this saves about 5.6 KB gzip, summed per module.bc6761f: The
hydrate: 'islands'shell check now rejects more interactive shells and also runs in dev:A hook is recognized through an import alias (
useEffect as onMount) and through a namespace or member call (O.useState()), and so is an aliased signal declaration.Every local function the shell references is checked, not only JSX tags: a component passed as a prop (
render={Item}) and a helper called to render output. A relative import passed into JSX is checked when it can render (a function, or a component-named value), including a namespace member such asUI.Buttonand a module-level alias of a local function. Strings, asset URLs and other plain values, including any export of a non-Octane module, are not shell output. A package component or a module-level wrapper such asmemo(...)passed as a value cannot be checked.A signal handle bound through a member (
{state.count$}), an import alias (count$ as live) or a module-level alias (const live = count$) is reported.In dev, an islands-only route whose shell needs client work now logs a warning naming the module, line and problem, once per problem, while the page keeps serving. The production build still fails on it.
f5644f7: Stop a query below a JSX
<Suspense>from reloading without end during hydration, and resume the server value that a.tsxcomponent reads in its returned JSX. A JSX<Suspense>or<ErrorBoundary>gave its try body a new identity on every render. A boundary retrying its own suspended first attempt therefore looked like it had received new children: it retired the attempt's queries and started fresh ones. When the browser had to load a query, for example one without a server seed, each load resolved into another retry and another load, and the page re-rendered indefinitely. The boundary now keeps one body and carries its children like a compiled@try, so only new children restart the attempt. On the server, a component's returned value rendered outside that component's signal owner, while the client renders it inside. Aquery$that a.tsxcomponent reads with.get()in its returned JSX was therefore seeded under the parent's identity and loaded twice on the server, and the browser loaded it again with a hydration mismatch. The server now renders a returned value inside its component's owner.5ef4f9a: Render private static JSX components through their compiled fragment again.
A private
.tsx/.jsxcomponent whose body returns only static JSX, and
whose every use is an attribute-free child of a returned host element, again
renders through that parent's template as a lite component call. Since async
signals, component descriptors carry their invocation site, and the proof
recognized only the older descriptor call. Every such component therefore fell
back to a full component slot plus a descriptor child slot. On the
signal-favoring chain, the JSX twin's shallow bump went from 10 full component
slots to 100.18c1b77: Reorder keyed lists without journaling every row. During a root render that can still be rolled back, a keyed reorder recorded each surviving row's previous position, four journal slots per row. Rotating a 1,000-row list was about 1.4 times slower than before root renders became undoable. The list's own shape record now restores each row's position from its original order on rollback. Recording the key order for that record also uses a single bulk copy. Rows that render their index still show the right position after a held render retries.
2b69387: Discard a mismatched server node inside a hookless component's host during hydration. A hookless component renders into the element its call sits in, which can differ from the parent of the enclosing block's range, for example a
<section>inside an@ifarm. When its template did not match the server node there, hydration kept the stale server node on screen and inserted the client's element beside it. Hydration now builds the client's element in that node's place, including when the node was the host's last child, and reports the mismatch once.07cc1d2: Keep a component-local
derived$orquery$when its captured values are unchanged. Since local declarations started following new props, every render ran the declaration's closure again, even with equal props. Aderived$that returned an object produced a new object on each render, so an effect or memoized child that depended on it ran on every parent render. The compiler now lists the render values a local declaration captures, as it does for a hook with an omitted dependency list. A render whose captured values are all unchanged keeps the accepted definition and its value without running the closure, and skips the per-render staging work. A changed value still reruns the declaration as before. A handle from a localsignal$with no key or a literal key is not a captured value, so aderived$that reads one keeps its value until the signal changes. When two declarations name one cell, such as a repeated explicit key, the first declaration in a render presents it on the server as well as in the browser. Server rendering previously threw because the second declaration replaced the cell's computation during the render.Declarations reached through a custom hook also no longer share a cell with an explicit key that spells out the hook's call path, such as
'user/h:…'declared directly in the component, and their path is no longer rebuilt for each declaration on every render.3d0e5e2: Re-select a component-local
query$when a later render's selector captures new props or state, and recompute a synchronous component-localderived$from its captured values. Previously both kept the closure from their first render. An equal selection keeps its request and adopts the new loader without refetching. A changed selection aborts the obsolete request and starts the new one when the render is accepted. A held transition or suspended attempt keeps committed readers on the accepted selection and reuses its pending request when it retries.f647cef: Keep destructured
.map()callback parameters writable on the client. A
templatexs.map(({ label }) => <li … />)compiles to a keyed row loop, and its
destructured fields were re-declared withconst, so a row handler that
reassigned one, such asonClick={() => { label = label + '!' }}, threwTypeError: Assignment to constant variableafter mount or hydration. Server
rendering already treated them as ordinary parameters. Identifier parameters
were unaffected.The same row prologue now keeps the kind of an authored
@forheader, so the
fields of@for (let { label } of xs)can be reassigned too. Output forconstheaders and identifier parameters is unchanged.8e90048: Keep every root of a hydrating
@ifor@switchbranch that the server rendered no range for. Such a branch adopts the server nodes at the cursor in place, and hydration bounded its content after the first root it adopted. When the branch was the whole content of another arm or of a component the client adopted, the rules that remove the server's leftover content then deleted the branch's later roots, which the client still renders, and reported a mismatch for them. A branch that was not deleted from still owned only its first root, so switching it off left the others on screen. The branch now ends after all the roots its template adopted. The server content after them is still removed and reported once, and a branch whose roots outnumber what the server rendered there is built on the client and reported, instead of hydrating without its later roots. This also covers a branch inside a component that was adopted in place.d496796: Compare a memo component's plain props with fewer ownership checks. The memo bail looked up own-property membership for every prop, so that an inherited
Object.prototypevalue cannot stand in for a removed prop. That lookup is now made only for values such a read can produce:undefined, functions and objects. A primitive prop that compares equal is already the object's own. Rows that pass mostly strings and numbers bail with one lookup instead of one per prop. An own__proto__prop holdingObject.prototypestill counts as a change when it is replaced.fdf54fe: A plain
.tsor.jsmodule that calls a hook-named method while it initializes, such asexport const initial = store.useValue(), now evaluates instead of throwingReferenceError: Cannot access '_h$0' before initialization. That covers top-level statements, optional chains, and a module-level class's static fields, static blocks, and computed keys. These calls run outside every render, so they keep their authored form with no hook slot. A hook call in an instance field initializer runs with each construction, possibly during a render, so a hook method or module-declared custom hook called there now keeps its own slot, as it would in a constructor.ce97a96: A plain
.tsor.jsmodule that calls a hook-named method or a custom hook inside a function it invokes in place, such asexport const value = (() => store.useValue())(), now evaluates instead of throwingReferenceError: Cannot access '_h$0' before initialization. That covers arrow and function expressions called directly or through.calland.apply, including async ones. A generator body waits for.next(), which a render may call, so its hook calls still get their own slot, as do those in a function invoked in place inside a component or hook.dc3e180: Drop the native array snapshots from client and server bundles that never map a
list. Both runtimes recordArray.prototype.mapand theArray[Symbol.species]
getter when they load, so a mapped list can tell the nativemapfrom one that
user code installs later. Bundlers could not prove those two reads free of side
effects. Every bundle that never reached the mapped-list code therefore kept
them as dead top-level statements, including a server bundle that only escapes
HTML.The reads are now marked pure, so bundlers remove them when nothing uses them.
A bundle that maps a list still takes both snapshots at load, before user code
can replace either intrinsic. A minimal client bundle shrinks by 89 bytes raw,
and the smallest server bundle shrinks from 654 to 571 bytes raw.e219d88: Set host refs before layout effects when a nested Suspense boundary reveals
after an update replaced its parent's pending retry. The nested boundary first
suspended inside that retry; once the update discarded it, the boundary stayed
hidden without its reveal bookkeeping, so refs mounted in it (including hosts
preserved from before the suspension) were never attached and a layout effect
reading them sawnull. A nested boundary that suspends while its parent is
already hidden also no longer calls a callback ref withnulla second time,
matching React.ed188d7: Leave a signal-capable only-child text hole alone when its value is unchanged.
A
{props.value}hole that is its element's only child compiles tobindSignalChildwhenever the value could carry a signal. That binding kept
the Text node as its token, so every render read the node'snodeValueto tell
an unchanged value from a changed one, and an unchanged value overwrote any
outside edit to the text. The binding now keeps the last written primitive, as
compiled text holes did before signal-capable bindings: an unchanged primitive
does nothing and leaves the DOM alone, and a changed one rewrites the same Text
node.afbad9f: Discard and report server content that an only-child text hole cannot adopt during hydration.
A text hole that is its element's only child adopts the server's leading Text
node. When the server rendered something else there, such as an element or a
comment, hydration kept that content and appended the client's text after it.
It reported nothing, and every later update wrote the text beside the stale
server nodes. Hydration now removes that content, so the element holds only the
client's text. It reports the mismatch throughonRecoverableErrorand, in
development, a located warning.suppressHydrationWarningsilences the report,
here and when the client renders nothing over such content. An empty server
frame is not reported, the same as an empty element.09cf476: Preserve the submitter's document order in function form actions and manual transition form status data. Include its entry before the native
formdataevent fires.0887a68: A delegated event listener that the browser runs synchronously during a render's
DOM writes, such asonBlurwhen a render disables or removes a focused input,
can now write signals. Before, it inherited the render's signal write guard and
threwSignalWriteError. Its reads no longer become dependencies of that render.
Subscribers notified by its writes also run outside the render, so they can
write signals and their updates schedule like any other event's. The rest of the
render stays write-guarded, and a listener called from inside a pure computation
is still rejected.b319c16: Plain
.tsand.jsmodules emit fewer custom-hook call boundaries. A call to a hook the module declares gets nowithSlotboundary when that hook reads no slot, calling nothing butuseContextand other such hooks. The same holds for a hook's only use when another module-declared hook makes it directly, provided that caller is used only by name and the omitted calls form no cycle. Every call still keeps its own state. Signal-reading modules and$hooks keep every boundary. Across the repository's bindings, this removes 37 boundaries.3514dc7: Production client builds now lower
useMemoanduseCallbackinline in plain.ts/.jshook modules that contain template literals,switchstatements,try/catch, default or namespace imports, import attributes, or method overloads. Before, any of that syntax kept the whole module on the slower callback-allocating path.A plain hook module also stays on that path, with its source unchanged, when the inline printer would emit different code. That covers
declare global, an emptyimport type {}, a cast assignment target such as(ref.current as any) = value, a non-null assertion that continues an optional chain such asbox?.item!.label, and a few TypeScript-only forms. Previously, such a module could fail to build, turn the type-only import into a side-effect import, throw where the optional chain should short-circuit, or lose authored types.3f1a7b2: Keep
import.defer()deferred in plain hook modules that production client builds reprint; it previously compiled to an eagerimport().985a81e: A custom hook in a plain
.ts/.jsmodule that calls another hook declared in
the same module now gives each call its own state, as a.tsrxmodule does.
Previously two calls to a local hook shared theiruseState,useRef,useId
and memo cells on the client, while the server kept them apart, so hydration
replaced the server's output. This applies to hook functions, hook-namedconstvalues and parameters, and aliases of them. Calls touse*
functions made while a module initializes, outside any function, are no longer
rewritten, so they no longer throw aReferenceError.cb71034: Hydrating an
@forthat the server rendered with items while the client has
none and builds its@emptyarm now reports the rebuild throughonRecoverableError, in production as well as development. The development
diagnostic is still logged once, now also when a pending sibling replays the
hydration attempt. A dormant<Hydrate>boundary whose list emptied before it
activated rebuilds it without reporting.69b02f1: Keep a module-private Context on the public provider in production builds when any of its providers receives element descriptors as children: under a
descriptorChildrencomponent orReactCompat, or inside a@{ … }function that code calls directly. Production builds previously threwTypeError: body is not a functionthere, while development rendered correctly.3041f5d: When hydration rebuilds an element whose server markup does not match, the
rebuilt element's dynamic attributes,class, andstylenow apply the client
values without a second report. Development builds used to log a false value
mismatch for each of them after the one structural mismatch. WithsuppressHydrationWarningon the rebuilt element, those client values were
dropped entirely, in development and production, because suppression kept the
"server" value, which was only the client template's empty placeholder. A value
mismatch on an adopted server element still reports, and suppression still keeps
the server value there.b58c783: When hydration rebuilds an element whose server markup does not match, the
@if,@switch,@for,@try,<Activity>, and<ErrorBoundary>blocks
inside the rebuilt element now mount as client content. They used to keep
hydrating against the server output that follows the mismatch. Development
builds logged a second, false mismatch for the same recovery, an@forcould
throw, and a block could take a following server element, list range, oruse()result as its own, so the rebuilt element showed the wrong content or
went missing. A block mismatch inside an element that hydration adopts from the
server still reports as before.02e4eb0: Keep a component root that hydration rebuilt after a mismatch where the server
node it replaced stood.When a component's template root did not match the server node at that
position, hydration reported the mismatch and rebuilt the root on the client,
but inserted it at the end of its range. Server siblings that later components
adopted then rendered before it, and server content after it that no client
sibling claimed stayed on the page. The rebuilt root now takes the replaced
node's place. Unclaimed server content after it in the same@ifor@switch
arm is removed as part of the one reported mismatch. A@switchor@ifthat
the server did not render keeps the rebuilt root inside its own range. A text
hole after the rebuilt root no longer throwsNotFoundError. A Suspense
boundary that resumes hydration after the rebuild places the root the same way
and keeps the server siblings it adopted.5f354d2: When hydration rebuilds an element whose server markup does not match, a text
hole inside the rebuilt element now shows the client's text even when its
element carriessuppressHydrationWarning. Suppression keeps the server's text,
but a rebuilt element holds the client template's placeholder rather than
server output, so the client text was dropped and the element rendered blank in
both development and production builds. Suppression still keeps a differing
server text on an element that hydration adopts from the server.02e4eb0: Keep the server node on screen until a root that hydration rebuilt over it
commits.When a template root did not match the server node at its position, hydration
removed that node as soon as it built the replacement. If the replacement then
suspended (ause()after the mismatched root), the server content vanished
while it waited. A deferred<Hydrate split={false}>boundary also retried the
suspended arm from the next server sibling, so the retry mismatched that sibling
too, reported a second mismatch and rebuilt it. The server node now stays until
its replacement commits in its place. A retry rebuilds over the same node
without reporting it again. A@switchor@ifarm that suspended this way
still owns the node, so a case change replaces it.ad203e9: When hydration rebuilds an element whose server markup does not match, it now
reports only the structural mismatch. A text hole inside the rebuilt element
used to compare the client template's own placeholder as if it were server
text, so development builds logged a second, false "server rendered text"
mismatch for the same recovery. A text mismatch inside an element that
hydration adopts from the server still reports as before.5e586e2: Fix a
<Hydrate>activation that suspends on a child inside a hydrated component fragment.Following components: when the activation resumed, each component after that one in the same block found the hydration cursor still inside the earlier component's server range. It removed or duplicated server nodes, even when the server rendered exactly what the client did. Those components now adopt their own server ranges.
Client-built children: a child that mounted inside content hydration rebuilt on the client now completes as a client render when it resumes. It no longer adopts a server node outside the rebuilt content.
Fragments with extra roots: a component fragment that holds more roots than the server rendered in its range, such as a component call followed by text, is now rebuilt and reported once. It is no longer adopted over another component's content. This also applies when the component suspends before its fragment is hydrated and then resumes. Below a passthrough root, whose ranges can sit one level off, hydration keeps its previous behavior.
cdc9b07: Hydrate a single-root component call that is its host element's last child when the server rendered a different element there. Hydration rebuilds the component's root in place of that element. In any production build, and in a development build for a component with hooks,
hydrateRootthrewNotFoundErrorand left the container empty: the rebuilt root was inserted before the server element it had just removed. The rebuilt root is now appended where that element stood. The mismatch is reported once throughonRecoverableError, and the server siblings before it keep their identity.90c209e: When hydration rebuilds a component's single root over a server element that does not match it, it now also removes whatever else the server rendered in that component's range, such as the rest of another component the server rendered there. Previously, that server content stayed on screen after the rebuilt root, and nothing reported it. The mismatch is still reported once, and the server nodes after the range keep their identity. This also applies when the rebuilding attempt suspended and a later attempt, or a
<Hydrate>boundary's resume, completes it.f34be34: Call
hydrateRoot'sonRecoverableErrorwhen hydration discards server content
for an@ifarm the client does not render, a branch range the server encoded as
something else, or a runtime host element's content. These recoveries already
rebuilt the DOM but reported nothing, in development or production. A list whose
server rendered its@emptyarm while the client has items now reports at the
list itself, once, including when a suspended boundary or root retries
hydration. A dormant boundary whose props changed before it activated still
repairs these ranges without reporting or warning.d5030cf: Stop a focused host's removal from running handlers of components that already
unmounted.Teardown disposes a component before it detaches the component's DOM, and
Chromium dispatchesfocusoutsynchronously while it removes a focused element.
That new event no longer starts handlers on hosts whose component has unmounted,
so a signal write fromonBlurno longer reportsScopeDisposedErrorand a
plainonBlurno longer runs for an unmounted component. Still-mounted
ancestors and other roots still receive the event, and an event that was
already being dispatched when the component unmounted keeps its handlers until
it finishes. A ViewTransition deletion keeps its committed handlers live until
it publishes.a9594d0: Skip removed hosts' handlers when a deletion cleanup moves focus before the
teardown reaches them.Deletion cleanups run parent first. A dialog that restores focus in its own
layout cleanup used to startonBluron an input owned by a child component
that the teardown had not reached yet. The same happened for an input in a
removed@forrow's value hole when an earlier component in the row moved
focus. A deletion now retires every host it removes before any of its cleanups
run, including a portal's content. Live hosts, including focused siblings of
the removed range, still receive the event.Handler publication no longer records an owner on each host, and a list clear
retires only the row that holds focus.15c7b97: A host rendered into a value hole, such as
{show && <div onBlur={…}><input /></div>},
no longer runs its handlers for thefocusoutthat the browser dispatches while
the host is being removed. The component that rendered it stays mounted, so
these hosts are now retired when they are removed rather than when a component
unmounts. This covers value holes, nestedcreateElementchildren, a host
whose tag changes, a root that renders a descriptor, and removals published by
a View Transition. Still-mounted ancestors keep receiving the event, and an
event that was already being delivered keeps its handlers until it finishes.3971840: Give a signal component below returned JSX the same instance on the server and
in the browser. A.tsxcomponent, or a plain function in a.tsrxmodule, that
returns host elements no longer adds a level of its own to the identity of the
signal and query declarations below it, and neither does a dynamic tag that
resolves to a host element. Hydration now resumes the server's cells and query
results for those components instead of starting fresh ones.Instance identity now follows authored component invocations only, so it no
longer changes when a module starts importingoctane/signals.d930afb: A root without a Suspense boundary that suspends while hydrating now reports a
hydration mismatch once. When its first attempt had already rebuilt mismatched
content, that attempt's diagnostics were published even though it was
discarded, and the retry reported the same mismatch again, so development logged
the warning twice andonRecoverableErrorfired twice. Some recoveries, such
as a renderable or only-child hole over server text, or a list the server
rendered differently, also left a half-built client subtree in place of the
server content while the root was pending. A suspended root attempt now leaves
the server content as the server rendered it, and its mismatch diagnostics andonRecoverableErrorreports are published only by the attempt that commits.d930afb: Keep server text and style values in place while a root without a Suspense
boundary is suspended during hydration, and report their mismatches once when
it commits. A first attempt that repaired a value and then suspended used to
show the client's text or style in the server's markup while the root was
pending, and the attempt that committed found the text already matching, so a
text mismatch never reachedonRecoverableErrorand development builds logged
no warning. Attribute, class, style, anddangerouslySetInnerHTMLwarnings now
wait for the committing attempt instead of logging once per attempt. When a
suspended root render, including a client update, rolls back after removing a
namespaced attribute such asxlink:href, the attribute now comes back in its
namespace.791f12e: Fixed-node
'use dom bindings'views that bind a URL attribute (href,src,action,formAction,xlink:href, …) now compile to the smaller scalar adopter, with the URL sanitizer attached only to those views. A URL view adopted throughadoptBindingsships about 32% less gzip. URL updates published by an early binding are now kept when the application later hydrates over it, as other attributes already were, instead of being reverted to the rendered value.0a14c20: Enumerate a deferred JSX value with the same keys as an ordinary element on the client. A JSX value that Octane defers until it renders also listed Octane's internal
__octaneInvocationSitefield inObject.keys,for...in, and object spread, so it no longer matched an element fromcreateElement. The server already hid this field. The client now hides it too, and the component's call-site identity still reaches its signals during rendering and hydration.9aa6736: Select the correct SSR option when its implicit value consists of dynamic text separated by hydration comments.
541ecdc: Drop unused module-load work from server and universal renderer bundles. The
server runtime built its ASCII async-identity table in a top-level loop, and
froze a shared empty snapshot list withObject.freeze. Bundlers cannot prove
either one free of side effects, so every server bundle kept both, including a
bundle that only escapes HTML. The universal renderer core had the same problem
with five frozen constants such as itsuseFormStatusresult.The table is now built by a pure-annotated
Array.from, and the frozen
constants are marked pure, so bundlers remove them when nothing reads them. A
bundle that encodes async identities still builds the table once at load, and
encoding is unchanged. The smallest server bundle shrinks from 571 to 477 bytes
raw. A universal root that never callsuseFormStatusshrinks by 65 bytes raw.5ef4f9a: Update a signal-capable text hole with one text write.
When a module renders signal-capable bindings, its later identifier holes
compile tobindSignalChild. For a plain primitive in an ordinary
marker-bounded hole, that binding reached the generalchildSlotpath on every
render. It now keepstextHoleUpdate's fast path: an unchanged primitive does
nothing, and a changed one rewrites the slot's Text node. Signal handles,
elements, raw-HTML hosts, and mode switches still take the general path.e93bb26: Re-run a component-local
derived$orquery$whose closure reads a variable that is declared after the declaration or assigned again. Such a value is stillundefined, or not yet final, where the declaration runs, so comparing it could not show that the closure would compute the same result. The declaration kept its first render's closure and value after that variable changed. A declaration inside the initializer of a variable its closure reads, such asconst api = { count$: derived$(() => api.items.length), items }, threw aReferenceErrorinstead. These declarations now reevaluate on every render, as they did before captured values were compared.97618d7: A calculated descriptor array rendered as an element's only child inside a
component's children body skips reconciliation again while it is unchanged. Since
renderable holes started accepting signal handles, these holes lost the compiler's
cached-array region. Every parent update then walked the unchanged list and its
items. The region now wraps the signal-capable binding. A signal handle, or any
other value that is not an unchanged plain data array, still rebinds on every
render, and switching back to a previously cached array rebuilds it.b5277d1: Keep directive-local bindings inside split
<Hydrate>boundaries.A
@catch (error, reset)reset parameter, an@forindex, an@switcharm
local, or acaselocal inside a handler is no longer passed from the parent
component into the split child, where it was undefined and threw aReferenceErrorduring hydration.@emptynow reads the enclosing scope
rather than the loop binding, so the outer value reaches the split child. A
boundary nested inside one of these scopes also keeps the directive binding
instead of picking up a same-named module declaration.5ef4f9a: Encode a keyed component descriptor's key once per server render path.
During SSR, a keyed component descriptor already carries its key in the
enclosing child identity scope. Since async signals, the component invocation
also appended a second encoding of the same key, which added one key encoding
and its UTF-16 scan per keyed descriptor row. The duplicate is gone. Replay
identity is unchanged, and signal instance keys still use the descriptor key.648a8d2: Render a server
@tryboundary without allocating a closure per boundary.ssrTrybuilt a small arrow on every call to label a presentation-binding
view's arm. It is now a module-level helper, so each boundary on each server
pass allocates one fewer closure. The emitted HTML is unchanged.63f07f5: Stop reporting a hydration mismatch when a
@switchor@ifcase changes
because a<Hydrate>boundary's props changed before it activated.When a boundary's captures change before it activates, the server HTML predates
the client's state, so hydration builds the changed case on the client without a
report. A case whose first node is an element or a component still calledonRecoverableErrorand logged "the client expected but the server
rendered …" in development. This happened when the case changed while the
boundary was pending after it had adopted the server's case, or when an early
activation rendered a different case than the server. Those cases are now
quiet, like fragments, text, and lists already were. A case the server did not
render is still reported when the captures are unchanged.1d33932: Stop reporting hydration mismatches, and stop keeping old server values, when a
<Hydrate>boundary's props changed before it activated.When a boundary's captures change before it activates, the server HTML predates
the client's state, so activation is meant to repair it silently. Several
recovery sites still calledonRecoverableError, in development and production,
and logged a development warning. This happened when a renderable hole's server
text became a component, when acreateElementchild list shrank, and when a
dynamic host tag changed. Every recovery site is now quiet under changed
captures. Unchanged captures still report a mismatch as before.Under changed captures,
suppressHydrationWarninganddangerouslySetInnerHTML
also kept the server's value after activation, which was older than the client's
state. An element could show old text and attributes next to siblings that
showed the new ones, anddangerouslySetInnerHTMLalso logged a false
development warning. Activation now writes the client's text, attributes, class,
style, and HTML in these cases. With unchanged captures, both still keep the
server's value.2b4b3fc: Keep the server DOM intact when a deferred
Hydrateboundary retries after its captures changed. If a mounted parent updated a boundary while its first activation was suspended, the retry rendered new content over server nodes that other blocks had already adopted. That content was a component swapped in by a dynamic call, a row inserted into a keyed list, or a renderable hole's new component, list, or text. Rows went missing, a sibling's server node was replaced, and the swap reported a false hydration mismatch. Content an update creates during hydration now builds on the client beside the adopted nodes, including when it suspends and a later retry resumes it, and nothing is reported.b98179f: Reveal a streamed Suspense boundary completely when its fallback renders an
@forlist (with items or its@emptyarm) or a'use dom bindings'view directly, rather than inside a host element. The inline swap script now counts those list and binding ranges when removing the fallback. Before, it stopped early: it left the fallback's close marker, which madehydrateRootthrowHierarchyRequestError, or left fallback nodes visible beside the revealed content. The optional streaming view-transition driver uses the same rule, so it now captures the exit of every fallback element.2b69559: Strong mode no longer reports
OCTANE_STRONG_EFFECT_STATE_UPDATEfor a state
update in acatchclause that can only run after itstryblock yields. The
common effect patterntry { setData(await api.get(id)); } catch (error) { setError(error); }now
compiles. Acatchstays synchronous when something before thetryblock's
first guaranteedawaitcan throw, such as a call,new,throw, or
iteration. The awaited call, calls it chains from throughthen,catch, orfinally, and calls written as array-literal elements of an awaitedPromise.all,allSettled,any, orraceare trusted to reject rather than
throw. Their callees and arguments still count.finallyclauses and statements
after thetryfollow the same rule.5ef4f9a: Let Strong-mode keyed
@forrows that log withconsoleskip re-rendering again.Since the change that keeps rows reading module state or mutable globals live,
a row body containing a diagnostic call such asconsole.log('row', item.id)
also lost its survivor skip, becauseconsoleis a host global. Every console
operation returns undefined, so the receiver of a statement-positionconsole.method(…)call cannot reach row output. Strong production builds now
reuse those rows again, as documented. The call's arguments are still row
reads. A module binding namedconsolestill disables the skip.1c44df5: Require Strong effect cleanup to actually cancel or ignore asynchronous state updates.
OCTANE_STRONG_EFFECT_DATA_FETCHnow covers any state update after anawaitor in a.then,.catch, or.finallycallback of effect-owned work, not onlyfetch. The returned cleanup must abort anAbortControllerwhosesignalreaches the request, or assign a flag declared in the effect that guards the update after the lastawait. Empty, opaque, and unconnected cleanups, component- or module-scoped flags, and ref flags are errors. Compatibility modules and emitted code are unchanged.f151614: Add
OCTANE_STRONG_EFFECT_HIDDEN_DEPENDENCY. Strong effect setup may no longer call a state getter, readcurrentfrom a value ref, or read a reassigned moduleletorvar, because none of them is an inferred dependency. Refs attached withref=or passed to a call, component, or hook remain readable, and reads in cleanup, deferred callbacks, anduseEffectEventcallbacks remain valid. Compatibility modules and emitted code are unchanged.f151614: Add
OCTANE_STRONG_EFFECT_RESOURCE_LEAK. Platform resources acquired in Strong effect setup must be released by the returned cleanup: event listeners on browser targets,matchMedialists, attached elements, and connections (by matchingremoveEventListeneror an aborted signal),on<event>handler properties, intervals and self-rescheduling timers,ResizeObserver,IntersectionObserver,MutationObserver, andPerformanceObserver,WebSocket,EventSource, andBroadcastChannel, and geolocation watches. User objects' subscriptions stay legal. Compatibility modules and emitted code are unchanged.1c44df5: Close zero-delay and custom-hook bypasses of Strong's synchronous effect update check.
OCTANE_STRONG_EFFECT_STATE_UPDATEnow treats callbacks that run before the next paint as effect setup:startTransition, auseTransitionstart function,queueMicrotask,.then/.catch/.finallyonPromise.resolve(value)orPromise.reject(),setTimeoutwithout a positive delay, and code after anawaitthat resumes without waiting on any path, such asawait nullorawait (flag ? load() : null). It also follows state tuples, updaters, callbacks, anduseTransitiontuples and start functions returned by same-module custom hooks, giving each hook call its own state, in.tsrx,.tsx, and plain TypeScript modules.requestAnimationFrame, timers with a positive delay, and external subscription callbacks remain event-driven. Compatibility modules and emitted code are unchanged.cda985b: Cache Strong-mode inline render expressions the way the same expression is cached when a
constnames it. In a production client build, an eligible expression in a host child hole, a host attribute, or a component prop, such as<output>{total.toFixed(2)}</output>or<List rows={visible(rows)} />, now recomputes only when its component-local inputs change, matching React Compiler. Expressions in@if,@for,@switch, and@tryarms, component children, and built-in boundaries still evaluate only when they render. Expressions that contain JSX, event handlers, refs, and keys keep their existing lowering. A component that returns JSX gets the cache only after an authored hook call, so a hookless one stays an ordinary function. Paralleluse()warm plans still start a child's request from the authored prop expression. Compatibility mode, development builds, and the server are unchanged.b6a414a: Strong mode now rejects imperative writes to DOM that the template renders.
When auseRefprovably names exactly one intrinsic element in the same
component, writingtextContent,innerText, or the child list of an element
with rendered children, or changing its class, a template-set attribute, or a
template-setstyleproperty, reportsOCTANE_STRONG_MANAGED_DOM_WRITE. WritinginnerHTML,outerHTML,insertAdjacentHTML(), orsetHTMLUnsafe()to a
rendered element reportsOCTANE_STRONG_RAW_HTML_WRITE; usedangerouslySetInnerHTML={trustHTML(html)}instead. Refs passed to components or
helpers, reassigned refs, and writes to DOM the template does not own stay valid.
Compatibility mode and emitted code are unchanged.09565b4: Expose the bundler compiler's per-module Strong decision as
strongModuleStatus(code, id).It reports whether a module compiles under Strong mode through its own
"use strong"directive, the application'sstrongpolicy, or both, using the
same rulestransformapplies.octane analyzeuses it for its Strong coverage
baseline.octane/compiler/bundleralso re-exportsfindLeadingJsxImportSourcePragma. Compiled output is unchanged.752028d: Strong mode now also reports a state value mutated outside render through a
whole state tuple passed to a local helper (including a destructured tuple
parameter), through an Effect Event called with
the state, and through a helper, updater, or reducer parameter that has a
default value. AuseLinkedStatereconciler that returns a literal now proves
array,Map, andSetmutators on that state, like a lazyuseState
initializer.b6a414a: Strong mode now rejects
document.getElementById(),querySelector(),querySelectorAll(), andgetElementsByClassName()when a literal selector
matches a literalidor class that the same component renders
(OCTANE_STRONG_OWN_MARKUP_QUERY). Attach a ref to the element instead. Portal
targets, dynamic selectors, and markup rendered by other components stay valid.
Compatibility mode and emitted code are unchanged.0339abc: Strong mode closes three gaps in its render determinism checks. Callbacks that
known array methods (map,filter,forEach,reduce,flatMap,some,every,find,sort, and their variants) run synchronously are now checked
as render, soitems.map(item => <li key={Math.random()} />)is rejected.crypto.randomUUID()andcrypto.getRandomValues()joinOCTANE_STRONG_RENDER_IMPURE_CALL, which now suggestsuseId()for element IDs
and a stable item ID for keys. Formatting a provableDateor anIntlservice
during render without an explicit locale and time zone reportsOCTANE_STRONG_RENDER_LOCALE_FORMAT, because server and browser output can
differ. Compatibility mode and emitted code are unchanged.e936a90: Strong mode now rejects scheduling work during render. Calling
setTimeout,setInterval,queueMicrotask,requestAnimationFrame, orrequestIdleCallbackwhile rendering reportsOCTANE_STRONG_RENDER_SIDE_EFFECT. The check covers direct calls,windowandglobalThismembers, unreassigned aliases, synchronous helpers, eager effect
and event-handler factories, and lazy state initializers. Registering the
callback is the side effect: a component can render more or fewer times than it
commits. Schedule from an event handler, or from an effect that cancels the
work in cleanup. Deferred callback bodies, events, effects, and compatibility
mode are unchanged, and valid Strong modules compile to the same output.752028d:
OCTANE_STRONG_STALE_STATE_UPDATEno longer reports state an Effect Event
captures, in its body or in helpers it calls synchronously, because an Effect
Event reads the latest committed values even when a timer or promise calls it.
A snapshot passed to an Effect Event from deferred code, values computed from
it, and timer or promise callbacks created inside the Effect Event are still
checked. So is a value computed from state at the call site and passed to an
Effect Event or helper from deferred code, likeapply(n + 1)after an await.54ff59e: Keep a resolved Suspense arm's server content in place while its first hydrating attempt is suspended, even when that attempt had to rebuild a mismatched node. The discarded attempt no longer removes server nodes or leaves client-built ones behind, and the attempt that commits reports the structural mismatch, and calls
onRecoverableError, once.25017f3: Keep server text and attribute values in a resolved
@tryarm while its
hydration is suspended, and report their mismatches once when it commits. An
attempt that repaired a value and then suspended used to show the client value
in the server's markup before the arm hydrated, and the attempt that committed
found the value already matching, so a text mismatch never reachedonRecoverableErrorand development builds logged no warning. Attribute,
style, anddangerouslySetInnerHTMLwarnings now also wait for the committing
attempt instead of logging from an attempt that is thrown away.c2fea71: Keep a pending
<Suspense>boundary'squery$loads when its parent renders again. While a boundary's first attempt was still waiting, any parent render that gave it new children (every render of a JSX<Suspense>, even with equal props) or new inputs (a compiled@try) restarted the attempt and retired its queries. Each such render started the load again, and a parent that re-rendered faster than the load settled, for example a ticking clock or typing above the boundary, kept the fallback on screen indefinitely. The same happened during hydration while the browser loaded a query the server had not resolved. The restarted attempt now takes over the earlier attempt's queries: an equal selection shares the load already in flight, and a changed one starts its own and aborts the earlier load. Hooks still start fresh from the new inputs. A component that declares asignal$or an asynchronousderived$still restarts its cells with the attempt, because those cells cannot follow new inputs.5ef4f9a: Stop allocating a closure for every transition and delegated event.
Since async signals, each
startTransitioncall created a native candidate
resolver closure. Each delegated event handler invocation also created a
callback closure, even when it did not enter another signal owner. The
transition resolver is now a module function that reads the active Action
batch. The handler callback is created only when the event enters a different
signal owner.27c2a12: Keep the streamed-boundary validator out of client-only apps that use
@try/Suspense. Mounting a boundary previously retained the check for a
streamed shell's pending<template>sentinel, and the hydration range-marker
helpers behind it, even in apps that never callhydrateRoot: about 400 B gzip.
Claiming that sentinel is now a hydration method, so client-only apps drop it.
Hydration behavior is unchanged.b682c2b: When hydration finds other server content where a component call expects its
range, the component is now built where that content stood, and only that
content is discarded: a later sibling component keeps its server range and
adopts its server nodes instead of being rebuilt with a second mismatch report.
Hookless components that return a fragment now recover the same way. A
component that suspends while it is built this way, or a fragment rebuilt over
another component's range whose hole suspends, now leaves the server content on
screen until the attempt that completes, which reports the mismatch once.56de31a: When hydration builds a component on the client because the server rendered no range for it, it now discards only the server nodes in that component's place and leaves the server range of a later sibling call for that call to adopt. Previously the discard ran into the sibling's range, removed its content and left a stray close marker behind, so the sibling rebuilt without a report of its own. The built component also stays ahead of those siblings instead of being placed at the end of its parent range.
ad12525: Adopt a component whose template has several roots when hydration finds the server's nodes without a range of its own, as when the server rendered a different
@ifarm with the same markup inline. In development builds the component adopted its first root, but the next component then read that root as its own and reported a mismatch. In production builds the component was rebuilt and reported as a mismatch. Both builds now adopt the server nodes when every root matches, report nothing, and continue after the last root, so the next component adopts its own server range.85cb387: Let universal-renderer modules declare
asyncand generator functions that JSX
never mounts. The universal compiler's synchronous-body restriction now applies
only once a function is actually compiled as a component — the@{ … }form, a
JSX-shaped return, or component usage — so a plain async helper in a.tsrx
file no longer fails the native-renderer build.517b61d: Reuse the Vite plugin's preflight classification of an unchanged module across
client environments that share one plugin instance, such as Vite'sbuilder.sharedPluginsor a dev server with several client environments.Before this change, every environment parsed and reclassified the same source
again, including host-owned TypeScript that the plugin passes through
unchanged. The plugin now keeps a small summary per module, keyed by
environment, specialization flags, and the full module ID, and checked against
a digest of the exact source. It holds neither the source nor its AST. The
summary is cleared when the compiler is reset and on every watch event. The
compiler still parses each module for its own transform, and output is
unchanged.
- 0.7.x
0.7.1
30 Sep 20269c324f6: A
@{ … }component passed toObject.assign, as inreturn Object.assign(Button, { Item })inside a factory function, compiles
again. Since 0.7.0 the compiler treated every function passed to a call as one
that code would call, and rewrote it to return JSX. Inside a module-level
function, that rewrite rejected a directive such as@ifin the component as
being in a module-level callback.Object.assignonly stores or returns the
function, so the component keeps its template body, with its props, control
flow, and captured variables, on the client and the server. The same holds forObject.defineProperty,Object.defineProperties,Object.freeze,Object.seal,Object.preventExtensions, andObject.setPrototypeOf. A
directive in a function passed to a call that may call it, such asxs.map(Row),
is still reported.73138de: Cancel injection producers when initial document seed validation fails during
prerender()orprerenderToNodeStream(), preserving the original validation
failure even if producer cleanup throws.
0.7.0
30 Sep 20264c1ca88: Keep the state of a hook declared after an early exit on the DOM renderer. A
hook's state lasts as long as the component,@forrow, or directive arm that
calls it; an exit only skips the rest of one render. On the DOM renderer, the
rest of a directive arm afterreturn;,return null;, orcontinue;, and the
rest of a component whose early return guards a single host element, ran in a
scope of its own. So taking the exit reset every hook after it, while the
universal renderer, value-returning components, custom hooks, and plainif
blocks kept that state. The DOM compiler now keeps it too: only the arm's output
is removed when the exit is taken. An effect after the exit is still cleaned up
while renders skip it and runs again once one reaches it.A class declared in setup is also passed to the directive arms nested below it
on the client, where reading it threw aReferenceError.4d8a93b: End a directive arm early on universal renderers the same way as on the DOM. A
continue;in an@forrow that no inner loop owns, at any nesting depth, now
ends that row. Before, it reached the arm's compiled function unchanged and the
module failed to load with "Illegal continue statement".A
breakthat targets the@foror@switcharound an arm failed to load the
same way. It is now the DOM compiler's compile error, with its location.Returning a value from an arm, such as
if (loading) return <Spinner />;, used
to render that value on universal renderers and is now the same compile error as
on the DOM. An arm's output is its final node on every renderer, and a component
can mix DOM and universal regions across a renderer boundary. If the rule
differed by renderer, the same arm would compile on one side of a boundary and
fail on the other. Render the alternative from an@if/@elsearm instead.55c2c01: Run each queued
useActionStatedispatch with the action that was current when
it was dispatched, matching React 19. Before, a rerender that supplied a new
action while a submission waited in the queue made that submission run the
replacement action. Payloads dispatched after the rerender still use the new
action, and an action error still continues the queue.
Minor Changes
4c1ca88: Keep the state of a hook declared after an early exit on the DOM renderer. A
hook's state lasts as long as the component,@forrow, or directive arm that
calls it; an exit only skips the rest of one render. On the DOM renderer, the
rest of a directive arm afterreturn;,return null;, orcontinue;, and the
rest of a component whose early return guards a single host element, ran in a
scope of its own. So taking the exit reset every hook after it, while the
universal renderer, value-returning components, custom hooks, and plainif
blocks kept that state. The DOM compiler now keeps it too: only the arm's output
is removed when the exit is taken. An effect after the exit is still cleaned up
while renders skip it and runs again once one reaches it.A class declared in setup is also passed to the directive arms nested below it
on the client, where reading it threw aReferenceError.4d8a93b: End a directive arm early on universal renderers the same way as on the DOM. A
continue;in an@forrow that no inner loop owns, at any nesting depth, now
ends that row. Before, it reached the arm's compiled function unchanged and the
module failed to load with "Illegal continue statement".A
breakthat targets the@foror@switcharound an arm failed to load the
same way. It is now the DOM compiler's compile error, with its location.Returning a value from an arm, such as
if (loading) return <Spinner />;, used
to render that value on universal renderers and is now the same compile error as
on the DOM. An arm's output is its final node on every renderer, and a component
can mix DOM and universal regions across a renderer boundary. If the rule
differed by renderer, the same arm would compile on one side of a boundary and
fail on the other. Render the alternative from an@if/@elsearm instead.
Patch Changes
55c2c01: Run each queued
useActionStatedispatch with the action that was current when
it was dispatched, matching React 19. Before, a rerender that supplied a new
action while a submission waited in the queue made that submission run the
replacement action. Payloads dispatched after the rerender still use the new
action, and an action error still continues the queue.38a5443: Report a compile error for a
breakorcontinuethat would leave a@{ … }
block, such asif (done) continue;in a child block inside an@forrow. The
parser accepted it because the jump sits lexically inside the loop, but every
target compiles the block apart from that loop, so the module failed to load
with "Illegal continue statement". A block is a nested template, not a directive
arm, so it has no early exit. Skip the row withcontinue;in the row's setup
before its output, or render the part to leave out from an@ifarm.A labeled
breakorcontinuethat would leave a directive arm, for a label in
the setup around it, failed to load the same way and is now a compile error too.0547835: A child
@{ … }block inside JSX that a callback in a component body returns
can now read the callback's params, on the client and the server, and
hydrates. The compiler used to declare the block's render function in the
component body, outside the callback, soconst row = (x) => <p>@{ const y = x + 1; <b>{y}</b> }</p>threwx is not definedat render. The block now compiles inside the callback,
keeps its hook state across parent updates, and its@if,@for,@switch, and@tryarms can read the callback's names. This also covers an
authored{() => @{ … }}child in such a callback, names bound in a nested
block of the callback, and the.mapcallback in setup or over a value that is
not an array.877a7a4:
hydrateRootnow adopts adjacent text children of acreateElementhost
without a mismatch.createElement('div', null, 'hello ', name)rendered two
text nodes on the client, but the server wrote them as one run of text, which
the browser parses as a single node. Every hydrate reported a recoverable text
mismatch, rewrote the first node, and built the second. The server now writes
React's<!-- -->separator between adjacent texts, including across nested
arrays and empty values. Hydration adopts each server text node and leaves the
separator in place, as React does.<textarea>,<title>, and other raw-text
content is unchanged, because a comment there would be literal text.A controlled or default
<select>value now also preselects, during server
rendering, an<option>without avaluewhose label is split across adjacent
texts, such as<option>{'Item '}{n as string}</option>. The label is compared
as its flattened text, as in React.68c094d: A host element created from an uppercase or mixed-case HTML tag, such as
createElement('DIV', null, text)or a dynamic<Tag>whose value is'H1',
now hydrates and updates like its lowercase spelling. HTML tag names are
ASCII case-insensitive, so the server's<DIV>parses as adivanddocument.createElement('DIV')builds one too. The client compared the tag
exactly, so hydration reported a recoverable mismatch and rebuilt the subtree,
and every later render replaced the element, which lost focus, selection, and
scroll. A host tree built from such tags was also rebuilt, rather than adopted,
when a component child appeared in it. The client now matches an HTML element's
tag in any casing. SVG and MathML tags stay case-sensitive (foreignObject),
as the DOM requires.c1a86ce: A child
@{ … }block inside a keyed element,<noscript>,<html>/<head>/<body>, or a host the HTML parser would repair (such as a<p>containing a<div>) now renders, on the client and the server, exactly as it does under an
ordinary element. The compiler builds those hosts as element descriptors, and
that path silently dropped the block, so<p key={id}>@{ … }</p>rendered an
empty<p>. A component that returned such a host with a setup-bearing block
kept the block on the client but not on the server, so hydration reported a
mismatch and rebuilt the subtree. A render-only block now groups its output
transparently. A block with setup runs in its own render scope and keeps its
hook state across parent updates.63baf8c: Read each component's source at most once when the development runtime looks up
its__octane_locmarker. Form diagnostics walk every ancestor block for each
mounted host element, and the lookup ranFunction.prototype.toStringplus a
regex on every visit without caching. Output compiled withoutdev: truehas
no marker, so every lookup missed and repeated: a 50-rowcreateElementlist
read component source 703 times per mount, and larger trees scanned megabytes of
source. Development mounts no longer pay that per element. Production builds are
unchanged.489db93: A
@{ … }function declared inside a component or another function, such asconst row = (v) => @{ <p>{v}</p> }orfunction row(v) @{ … }, can now be
called directly:{row(props.v)}and{rows.map(row)}render what(v) => <p>{v}</p>renders, on a client mount, on the server, and through
hydration. The compiler compiled such a function only as a render body that the
runtime calls with a render scope, so a direct call threwCannot read properties of undefinedon the client, and the server rendered its output
without the range hydration expects. When code calls it directly, the function
now compiles to the returned-JSX form thatfunction f() @{ … }is shorthand
for. It still renders when passed as a{row}child, a component, or a portal
body.687f584: A module-level
@{ … }function, such asfunction Row(v) @{ <p>{v}</p> }orconst row = (v) => @{ … }, can now be called directly from the same module:{Row(props.v)},Row.call(null, v), and{items.map(Row)}return what the
returned-JSX formfunction Row(v) { return <p>{v}</p>; }returns, on a client
mount, on the server, and through hydration. The compiler compiled such a
function only as a component body, which the runtime calls with a render scope,
so a direct call threwCannot read properties of undefined (reading 'slots')
on the client, and the server rendered its output without the range hydration
expects. A directly called function now also gets its returned-JSX form, which
its body runs when it is not called to render. Its setup and hooks run in the
caller, an earlyreturn nullreturns null, and its JSX resolves where the
value renders. Rendering it as<Row />or a{Row}child still runs the
compiled template, and modules without direct calls compile unchanged.1b8c949: A child
@{ … }block written directly in a directive body with no element
around it, such as@if (x > 0) { @{ const y = x + 1; <b>{y}</b> } }, now
renders on the client and the server, and hydrates. This covers@if,@else,@switchcases,@try,@pending,@catch,@for, and@empty
bodies. The compiler used to treat the block as a setup statement and discard
it, so the body rendered nothing. The block is now that body's output, as it is
among element children. A render-only block groups its output transparently. A
block with setup runs in its own render scope, can read the body's own locals,
and keeps its hook state across parent updates.04df7e9: End a directive arm early the same way wherever the exit sits in its setup.
return null;is now the same exit asreturn;, and so is an exit inside a
nestedif, block,switch, loop,try, or labeled statement. Before, only a
top-levelif (c) return;(orcontinue;in an@forbody) was lowered: the
server rendered any other exit's literalnullorundefinedas text, and a
client update that took the exit left the arm's earlier output in place.A keyed
@forrow that can exit early no longer uses its element as the row
boundary, because the row renders no element when it exits. Rows with an exit
could previously crash (insertBeforeof null) when a hidden row moved, and fail
to update after hydration. The same applies to a row or component whose root is
an@if/@elsewith an arm that can exit.Returning a value from an arm, or a
breakthat targets the@foror@switch
around it, is now a compile error with its location. These previously rendered[object Object]on the server or emitted JavaScript that failed to load.489b121:
hydrateRootnow adopts a container filled with the wholehtmlof a
body-only render that hoisted document metadata. The defaultheadChannel: 'fold'prepends each hoisted<title>,<meta>, or<link>
ahead of the body markup, as React 19 does. Hydrating that container reported
a recoverable hydration mismatch and rebuilt the entire root on the client
(in development it also logged that the client expected the root element but
the server rendered a comment).hydrateRootnow moves each folded metadata
entry intodocument.headand adopts it there, which matches a client render.
Folded Float stylesheets, scripts, and resource hints stay in place and are
skipped.ce47fae: A React-style
keyattribute on the only root of an@forrow now compiles
exactly like the header spelling@for (…; key expr), for intrinsic and
component roots alike. Before, an intrinsic root whose content read anything
not provably stable, such as{props.render(row)}, was lowered to a keyed
element descriptor. That cost about five times as much per update, and because
the row was still treated as a single node, removing or reordering such rows
left stale elements behind. A component root lost its row memo. The compiler
now also rejects a row key that reads a declaration from inside the loop body,
which previously failed at runtime with aReferenceError.b2e4b9a: The compiler now rejects an
@forrowkey={…}attribute that reads a class,
an enum, or avarhoisted out of a nested block in the row body. Before, only
top-levelconst,let,varandfunctiondeclarations were caught, so
such a key compiled to a key function that threw aReferenceErroror silently
read an outer binding of the same name. The check now resolves the key's names
by scope, so a name the key binds itself, such as a callback parameter, still
compiles.8de664c: Fix
FragmentInstance.blur()for owned children focused inside a shadow root or a same-origin iframe portal. It now checks each child's own focus root instead of only the fragment marker's document.5cc6b64: Recover when hydration finds different server content where a nested
multi-root (fragment) template component renders. A renderable{expr}hole
whose server value was text but whose client value is a fragment component,
rendered directly or returned from a plain component, crashedhydrateRoot
withHierarchyRequestError: Node can't be inserted in a #text parent. When the
server had rendered a different fragment component in that hole, hydration
kept the server's elements without reporting anything. The fragment now checks
its first root against the server node, reports the mismatch toonRecoverableErroronce, logs the development hydration-mismatch warning,
discards the server content it would have adopted, and builds its markup on
the client. A matching server fragment is still adopted in place.0f8cb49: Discard and report server list content that the client's items cannot adopt
during hydration. Several list shapes previously kept stale server content on
screen, or rebuilt it without reporting toonRecoverableError:A renderable hole whose client value is an empty list (``) kept whatever
the server rendered there visible. Hydration now discards it and reports the
mismatch once.A list item whose server content cannot be that item now discards the rest
of the list's server content, builds the client items, and reports once. This
covers a bare server element where the client item needs a range of its own
(the stale element stayed visible) and a bare element of another tag (it was
swapped silently).A list or
@forwith more client items than server items builds the extra
items and now reports once, with one development warning in place of one per
item.A renderable list with fewer client items than server items now discards the
extra server items and reports them, as@foralready did.
A boundary that retries hydration after suspending does not report these
again. A dormant<Hydrate>boundary whose captures changed before it
activated repairs its list without reporting, including when a@forrendered
fewer items than the server.7fa3a2d: Report a renderable value that hydration rebuilds once, including when it
suspends and its boundary retries. When the server rendered nothing where the
client renders a list, a fragment, or a keyed element, or where a component
returns a list or an element, hydration built the value without reporting it toonRecoverableError, and development logged one warning per item. It now
reports the recovery once, with one development warning that names the hole,
the returning component, or the list's host. A list whose items all render
nothing still hydrates silently.A boundary that retries hydration after its value suspended no longer reports
or warns about content an earlier attempt already rebuilt. The retry rebuilds
that content instead of adopting what the failed attempt left. Adopting it had
duplicated an element a component returned in place of its server text. Two
further shapes rendered with component children no longer throwNotFoundErrorduring hydration: an element a component returns where it
rendered nothing on the server, and a list item whose server range is empty.
A list item whose server range holds another element now reports its rebuild.aa6753b: A
@{ … }function passed as a call argument, such as{rows.map((row) => @{ <li key={row}>{row}</li> })}, now renders what(row) => <li key={row}>{row}</li>renders, on a client mount and update, on
the server, and through hydration. The compiler compiled such a function only
as a render body that the runtime calls with a render scope.mapcalled it
with the row index in that position, so a client mount threwCannot read properties of undefined. A@{ … }function passed inline to any call ornew, or a nested@{ … }helper passed to one (rows.flatMap(row),run(row)), now compiles to the returned-JSX form that@{ … }is shorthand
for. Render props,{fn}children,<Tag />uses, portal bodies, and the
component given tomemoorcreateElementstill render through the compiled
template.ce11f82: Read a reassigned variable's value from when a JSX value was built, matching
React. Octane renders the non-literal children and props of a JSX value later,
so a variable assigned again in between showed its later value: a counter
incremented in a.mapcallback rendered its final value in every row, andcontent = <Frame>{content}</Frame>nested the wrapper inside itself until the
stack overflowed. The compiler now captures such variables when the JSX
evaluates, on the client and the server, so hydration agrees. Event handlers and
other functions inside the JSX still read the variable's current value. JSX a
module-level function declaration returns when called directly still reads at
render, as documented indocs/differences-from-react.md.bd21050: Release streamed signal async iterables that resolve after an SSR response is cancelled or its request aborts.
5575ff6: Emit smaller client code for
.tsrxtemplates and compiled.tsxcomponents
without changing what they render or the work they do. Component call sites no
longer pad their arguments withundefined, text-hole updates pass only the
arguments an update uses, and binding bags read their DOM nodes directly
instead of through copied locals. Across the compiler's test fixtures, minified
output shrinks by about 3% and gzipped output by about 2%.5f14459: A child
@{ … }block inside JSX that a module-level callback returns now
renders, on the client and the server, and hydrates. The compiler used to drop
it silently, soconst row = (x) => <p>@{ const y = x + 1; <b>{y}</b> }</p>
rendered an empty<p>. A render-only block now groups its output
transparently. A block with setup compiles in place, as the{() => @{ … }}child it is shorthand for. It closes over the callback's
params, runs in its own render scope, and keeps its hook state across parent
updates. An@if,@for,@switch, or@tryin that block's output, or in
an authored{() => @{ … }}child in a module-level callback, can now read
the callback's params. Its arms used to be hoisted to module scope, where
those params do not exist.9230292: A
@{ … }component that declares parameters afterprops, such asfunction Row(props, extra) @{ … }, now renders. The runtime calls a component
as(props, scope, extra), but the compiled body listed the scope after every
authored parameter, so rendering<Row />threwCannot read properties of undefined (reading 'slots')on the client. The body now always takes the scope
second, and each later parameter holds the argument at its position, exactly as
in the returned-JSX formfunction Row(props, extra) { return … }. While a
component renders, the parameters afterpropsreceive internal values rather
thanundefined, so pass a component's inputs through props. A rest parameter
after the first, and a parameter default or destructuring pattern, bind the same
way.e4974cc: Treat
const Row = React.memo(Component)the same as a namedmemoimport whenReactisimport * as React from 'octane'. The compiler only recognized the
named form as an immutable memo wall, so a@forrow rendering a
namespace-imported memo component lost its dependency-compare fast path and
re-rendered every row on each parent update. Such rows now keep the same@forflags as the named form. A namespace import from another module stays
opaque.19f07e0: Fix a
ReferenceErrorin DOM-compiled nested templates. Inside a setup-bearing@{ … }child, a() => @{ … }sub-template (including acreatePortalbody),
or afunction F() @{ … }declared in a component, an@if,@for,@switchor@tryarm, or a lifted event handler, could read the template's
own locals or parameters, or the enclosing component's locals, as unbound
identifiers on mount, hydration, update, or when the event fired. The compiler
now passes those names to the code it hoists. A child local that shadows a
stable parent binding, such as a state setter, is also no longer treated as
stable, so a changing handler takes effect.898820a: Discard stale server text when hydrating a renderable
{expr}hole that is its
host element's only child and whose client value renders nothing (null,undefined,false,true, or''). Hydration kept the server's text, so a
later value rendered beside it (<div>AB</div>) and nothing was reported. Anullvalue in the root component recovered only by abandoning hydration for
the rest of the root, which duplicated the text of a later only-child hole. The
hole now removes the server content, reports the recovery toonRecoverableErroronce, and names its source location in the development
hydration-mismatch warning. A dormant<Hydrate>boundary whose props changed
before it activated still repairs the hole without reporting it.25cc659: Discard stale server text when hydrating a renderable
{expr}hole that is its
host element's only child and whose client value is an element, a component, or
a list. In the root component, hydration gave up on the rest of the root, so a
later only-child hole rendered its text twice (<b>tt</b>) and the development
warning named the wrong cause. In a nested component, the value rendered beside
the server text and nothing was reported. The hole now removes the server text,
reports the recovery toonRecoverableErroronce, names its source location in
the development hydration-mismatch warning, and the rest of the root keeps
hydrating. A value that suspends is reported and built once when its boundary
retries, a textarea keeps its server text as its default value, and a dormant<Hydrate>boundary whose props changed before it activated still repairs the
hole without reporting it.8fb96a0: Hydration now recovers when the browser's HTML parser has repaired
server-rendered markup, such as a<div>inside a<p>. The server wraps
such an element in a hydration range, so the parser's repair stays inside it.
The client rebuilds the element once, discards the nodes the parser split out,
and reports the recovery throughonRecoverableError, with a development
hydration-mismatch warning. Previously the content appeared twice and the stale
copy stayed on screen, a later sibling's bindings could land on a stray node,
and a repaired element in the root component could blank the page. Hydrating an
element with component children where the server rendered nothing no longer
throws.699e363: Remount a portal's child element when its key or type changes.
createPortal(<Editor key={id} />, target)kept the same component instance whenidchanged, so its DOM and any value typed into an uncontrolled input survived a keyed reset. Switching the child between component types, or between a host element and a component, reused the old DOM the same way. The portal now remounts its child for a new key or element type, as React does. An unchanged key and type still preserve the child, and the portal's own third-argument key works as before.84d2eaf: Release a portal's previous range when it rebuilds under a mounted owner.
A
{createPortal(children, target, key)}at JSX child position rebuilds its
whole subtree whenever its key, target, or child element type changes. Each
rebuild used to register the new portal alongside the old one instead of in its
place, so an owner that stayed mounted, such as a dialog host keyed by the
selected id, kept every replaced portal and its detached start and end markers
alive until it unmounted. The rebuilt portal now takes over the previous one's
registration.A transition that rebuilt the portal and then suspended also left the restored
portal unable to receive delegated events, such as clicks inside a dialog,
because rolling back released the target's event listeners twice. Rollback now
restores the previous portal's registration and releases the abandoned one
exactly once.ca7d55d: Release late-resolving streamed RPC iterables when an unread response is canceled, aborted, or times out.
5bc3af7: Discard stale server content when hydrating a renderable
{expr}hole whose
client value is text or empty. If the server rendered an element, component, or
list into the hole but the client rendered a string, number,null, orundefined, hydration kept the server's content next to the client value
through every later render and reported nothing. The hole now keeps at most one
server text node, removes the rest, reports the recovery toonRecoverableError,
and names the hole's source location in the development hydration-mismatch
warning. A dormant<Hydrate>boundary whose props changed before it activated
still repairs the hole without reporting it.94ba1b6: A
@{ … }function with a rest parameter, such asexport function Join(...parts: string) @{ … }, now loads and renders on
the client and the server, and hydrates. The compiled body appends its render
scope parameters after the authored ones, so it emittedfunction Join(...parts, __s, __extra), and the module threwSyntaxError: Rest parameter must be last formal parameterwhen it loaded.
The server did the same for a returned-JSX function with a rest parameter,
even a helper that code only calls directly. The rest parameter now holds what
the returned-JSX form's rest parameter holds: a direct call's own arguments,
and for a component render the runtime's(props, scope, extra)arguments
from its position on. A TypeScriptthisparameter on a@{ … }function no
longer takes the place of the props, which had crashed the client render.03e7ba0: Lower an early exit inside a directive arm the same way under returned JSX as in
a template body. Forreturn <div>@if (x) { if (c) return; <b /> }</div>, the
client kept the arm'sreturnas a literal JavaScript return, while the server
rendered the rest of the arm as a nested range. Hydration reported a mismatch and
rebuilt the subtree. A client update that took the exit also left the arm's
earlier output in place. The same applied to@else,@switchcases, andreturnin@forbodies, and to directives inside JSX values stored in
setup.ddb655c: Discard stale server text when hydrating a renderable
{expr}hole whose server
value was text but whose client value is a component that returns another
component's element or list. Hydration kept the server text beside the client
element and reported nothing. When the returned component rendered the server's
text instead, a later switch to a different returned component left that text
behind. The server text now stays inside the returned components' range, so it
is adopted as their text, or removed and reported once toonRecoverableError
with the development warning naming the component that returns the element. The
same holds when a component in the chain suspends during hydration.4ebe8d4: A component that returns a keyed element, a
<noscript>, or a document element
such as<body>from an ordinaryreturnnow renders that element's@if,@for,@switch, and@trychildren on the client, including directives
nested in a child element, fragment, or component inside it. The client used to
mount the element empty on every render while the server rendered its content,
and a directive inside a<>…</>fragment there failed to compile. A.tsx
component returning a keyed element with a.map()child rendered it empty in
the same way. The same element in a@{ … }body was not affected.e325a83: A component that returns a keyed element, a
<noscript>, or a document element
such as<body>or<html>from an ordinaryreturnnow hydrates by adopting
the server element. The client used to mistake that element's own server range
for the component's, so it warned about a list mismatch and rendered a second
copy beside the server element, or silently duplicated a<noscript>or<body>, and a returned<html>failed to hydrate. Switching such a component
between that element and other output, such as text, no longer leaves the old
element behind. The same element in a@{ … }body was not affected.d78f279: Resume server-resolved signals such as
query$when their component renders
inside JSX that a parent returns withreturn <…/>rather than an@{}body.
The browser used to call its own loader, replace the server text, and report a
hydration mismatch, for two reasons. A component placed beside a directive in a
returned fragment or host element compiled to a different call-site id on each
side. Children of a returned fragment, array, or keyed element also missed
their list position in the server's signal identity. Both sides now agree, and
explicitly keyed de-opt children no longer serialize a JSON key per item on the
server.ddb655c: Discard stale server text when hydrating a renderable
{expr}hole beside other
children whose server value was text but whose client value is a list, a
fragment, a keyed element, or a portal, or a component that returns an element
or a list. Hydration kept the server text next to the client value, so the
content appeared twice (a portal's hole kept collecting text on later renders),
and nothing was reported. The hole now removes the server text, reports the
recovery toonRecoverableErroronce, names its source location (or the
returning component's) in the development hydration-mismatch warning, and the
rest of the root keeps hydrating. A dormant<Hydrate>boundary whose props
changed before it activated still repairs the hole without reporting it.8fb96a0: Keyed
@forrows now reorder and remove correctly when a row's only element is
not built from the template. That covers a<noscript>, a tree the HTML parser
would repair (such as a<div>inside a<p>), a<meta>or other element
hoisted into the document head, and an element with its ownkeyinside an@ifor@switcharm. It also covers components whose root is one of these.
Octane used to treat such a row as its single element, so removing rows could
leave elements or comment markers behind, reorders could misplace rows, and a
hoisted row could crash the list. Server-rendered keyed and<noscript>rows
now hydrate by adopting the server elements too.d7ffa13: Server rendering no longer wraps the only renderable child of a host that has
a spread in hydration markers, matching how the client mounts that child.
Inside a<textarea>, whose content the HTML parser keeps as text, those
markers became part of the default value:<textarea {...rest}>{value}</textarea>
showed<!--[-->A<!--]-->before hydration, and hydration then reported a text
mismatch. The markers also stopped a server-rendered<option {...rest}>{label}</option>
without avaluefrom matching its select'svalueordefaultValue, so the
wrong option was selected until hydration.Updating the child of a
<textarea>that has a spread also no longer empties
it. Authored textarea children are a live text binding, but every update after
mount cleared the default value when the spread supplied novalueordefaultValue, which detached the child's text.32c1bf4: Let
prerenderfinish when a resource reader keeps rethrowing a thenable that
has already settled until a timer or I/O callback updates its state. These
retries used to run on microtasks, so the render failed after 50 passes before
the callback could run. Once the render has seen that thenable settle, it now
retries on a timer that backs off from 1ms to 100ms. These retries no longer
count toward the pass limit, and a reader that never recovers fails once the
stall laststimeoutMs. When a pass limit is reached on thenables thrown
outsideuse(), buffered and streamed errors now name that cause instead ofuse().7ee5f1a: Let
renderToPipeableStreamandrenderToReadableStreamfinish when a resource
reader keeps rethrowing a thenable that has already settled until a timer or I/O
callback updates its state. A lag longer than about 50 event-loop turns used to
fail the stream after 50 passes. These retries now back off on the same timer asprerender, do not count toward either streaming pass limit, and stop waiting
as soon as another boundary's data arrives. A reader that never recovers fails
once the stall laststimeoutMs. When the pass limit before the shell is
reached on thenables thrown outsideuse(), the shell error now names that
cause.608ff43: Report
"use strong"inside a function or component body asOCTANE_STRONG_DIRECTIVE_PLACEMENT. Strong mode applies to a whole module,
but a function-body prologue parsed as an ordinary directive and was ignored, so
the module compiled in compat mode with no diagnostic and Strong checks never
ran. The compiler,slotHooks, and the Volar diagnostics now reject it and
ask for the directive at the top of the file.09c58dc: A keyed
@forrow no longer leaves a partial copy of itself behind when a child
suspends or throws during the row's first render. When the row's only root is an
element, a component whose only root is an element, or an@ifwhose branches
are single elements, the row's element was already in the list when the child
suspended. Nothing owned it, so the retry rendered the row again beside the
stale one, or ahead of the rows that came before it. The partial element is now
removed with the rest of the row, whether the row is mounted, inserted by an
update, or rendered by a client that has more rows than the server. During
hydration, the retry no longer adopts the partial element as server output.28a3636: Stop a Suspense retry from livelocking the page when it keeps suspending on a
promise that has already settled. This happens when a resource reader throws a
resolved promise before its own state catches up, or whenuse()reads a
thenable whosestatusReact does not recognize, such as'resolved'. Each
retry used to run on that promise's next microtask, so timers and network
callbacks never ran and the suspension could never end. A retry now waits one
task once the promise's settlement has been seen, as React's Scheduler does. A
promise's first settlement still retries right away.3f9b16b: A
<textarea>whose children come from a spread or achildren=prop, such as<textarea {...{ children: value }} />, no longer empties on updates after
mount, whether it was mounted on the client or hydrated. Its default value now
follows every render, as it already did for authored JSX children. A signal
update to another prop in the same spread no longer clears it either.25b6e6d: A
<textarea>with more than one child now renders its children as one run of
text on both the client and the server. The HTML parser keeps markup and comments
inside a textarea as literal text, so the hydration markers used between
children became part of its value.<textarea>hello {name as string}</textarea>
threw "Cannot read properties of null" on a client-only mount. Server rendering
showedhello <!-- -->Auntil hydration, which then reported a mismatch.<textarea>hello {name}</textarea>and<textarea>{a}{b}</textarea>mounted
with a literal<!>in the value, and hydration rebuilt the textarea. The same
text rule applies to textareas made bycreateElement, stored
JSX, and achildrenprop from a spread. These no longer report a hydration
mismatch or serialize array children with markers. A signal handle in a
compiled textarea's children stays live.A textarea child must be text. Strings and numbers render,
null,undefined
and booleans render nothing, and arrays or iterables of text are concatenated.
An element, function or other object now throws a clear error on both sides.
Before, the client inserted an element that the textarea's value ignored and the
server wrote it as literal text. An element, component, or template directive
written inside a<textarea>is a compile error.9d322ef: Give
useActionStateon universal renderers the React 19 queue semantics the
DOM runtime already has. Dispatches now run one at a time, each receiving the
previous completed result aspreviousStateand running the action that was
current when it was dispatched. The dispatcher keeps one identity across
renders, andisPendingstays true until the queue drains. An action error now
reaches the nearestuniversalTryboundary (or the root'sonUncaughtError),
keeps the prior state, and lets later queued dispatches continue. A
function-valued state is now stored rather than called.d455cb1: Universal renderers now treat a React-style
keyattribute on the only root of
an@forrow as the row key, exactly as the DOM renderer does. It takes
precedence over a header key and compiles byte for byte like@for (…; key expr), for intrinsic, component and Activity roots. Before,
universal renderers read only the header, so a row keyed by its root attribute
fell back to a positional key: its state followed the slot rather than the item
across reorders, and the attribute stayed on the root as a separate key that
kept the row off the static-prop, owner-free and template-program row lowerings.
A root key that reads a name declared inside the row body is now a compile
error.40f4827: Import only the renderer helpers a universal module's output references.
Universal compiler output used to import every region helper from its renderer
module, whether or not it called them. A standalone renderer runtime exports only
the helpers it implements, so bundlers such as esbuild rejected the unused
imports. The Lynx main thread hit this afteruniversalBlockwas added: even a
plain<view />component failed to link.octane/universalandoctane/universal/nativenow also exportuseFormState,
the pre-19 name foruseActionState. The universal compiler already accepted
that import fromoctane, but no universal runtime provided it.4180828: Stop a universal renderer Suspense retry, including in Lynx, from livelocking
when it keeps suspending on a thenable that has already settled. Before, each
retry ran on that thenable's next microtask, so timers and native callbacks
never ran and the suspension could never end. This happened when a thenable's
owner publishes itsstatuson a later task, or when a reverse-region DOM child
routed the same suspension back to its universal owner. A universal root inside
a DOM Suspense boundary could loop the same way. A retry now waits one task once
the thenable's settlement has been seen, as React's Scheduler does. A first
settlement still retries right away.Universal
use()no longer rewrites a thenablestatusit did not set. Before,
it replaced a status React does not recognize, such as router-core's'resolved', with its own'pending'/'fulfilled'tracking. It now leaves the
status alone and treats the thenable as pending, matching React and the DOM
runtime.c587109: Give
useOptimisticon universal renderers the React 19 semantics the DOM
runtime already has. It previously ignored every passthrough value after the
first and never reverted an optimistic update. An optimistic update now shows
at once, even inside a transition, and rebases onto each new passthrough. It
reverts in the same commit as the transition it was dispatched in, or the
pending async action it follows, including when that action fails. An update
dispatched outside any transition shows once and then reverts. Without a
reducer, a function action updates the pending state, as inuseState.
Dispatching an optimistic update while its component renders now throws, as in
React.ba483f4: Without a reducer,
useOptimisticnow applies a function action as an updater
that receives the pending optimistic state, asuseStatedoes and as React 19
does. It previously stored the function itself as the optimistic state. Updaters
now chain, and they rebase onto each new passthrough while the action is
pending. The dispatch type without a reducer is now(action: State | ((pendingState: State) => State)) => void.c37f922: Start a
<ViewTransition>on the live document when a boundary is nested inside a component's compiled template that has not been inserted yet. The staged clone still belongs to the inert<template>document, so Octane picked that document as the transition owner. ItsstartViewTransitionreturnsnullbecause it has no browsing context, and the commit threwCannot read properties of null (reading 'ready'), leaving the old screen in place. The owner now resolves through the parent block's host when the boundary's own parent belongs to a document without adefaultView.007691e: Keep an unchanged nested
<ViewTransition>paired when an ancestor boundary animates.The nested boundary was captured on the old side only, so its old snapshot played the boundary's
updateclass on its own, over the ancestor's new snapshot that already paints the same element. It now keeps its name on the new side too, as React does, and cross-fades in place.
- 0.6.x
0.6.3
28 Sep 2026df0a086: JSX returned by a direct call of a named
functiondeclaration now resolves its non-literal props and children where the value renders, the same as JSX returned by an arrow function or method. Previously such a call read context and other values during the call, and the server rendered the value to HTML immediately, so a server-rendered fragment root or.map()row could disagree with the client. Rendering the same function as a component (<Row />) is unchanged.962ed16: Direct calls to hot-reloadable exports now behave the same in development as in production. Calling an exported return-JSX function directly with a second argument, as in
renderRow(item, index), no longer makes the next hot update throwTypeError: Cannot read properties of undefined (reading 'disposed'), and anullsecond argument no longer throws at the call. Direct calls also receive theirthisand every argument unchanged, where development previously passed exactly three arguments, dropping extras and padding missing ones withundefined.9441ccb: Node now loads a build that reads
process.env.NODE_ENVonce per module instead of at every development check. ESM imports get it through a newnodeexport condition, andrequire()gets it through the CommonJS build. Unbundled server rendering is faster, and deleting theprocessglobal after import no longer turns framework errors intoReferenceError: process is not defined. SetNODE_ENVbefore the first Octane import in Node. Browser bundles are unchanged.
0.6.1
28 Sep 20269b6c3a4: Include imported theme styles in server output when
$classor a class-map entry was captured at module scope or cached during an earlier render. Collect the theme and its dependencies when the class is rendered, preserving request-specific styles, dependency order, and CSP nonces.5f58b63: Production builds now report signal-engine, hydration, and DOM-binding errors as compact
Minified Octane error #<code>messages instead of shipping their full English text. Development messages are unchanged.1e4f1ee:
createElementnow acceptsnullas its config understrictNullChecks, matching React'sprops?: P | nullsignature. A barenullconfig infers props from the element type alone, and a non-null config still enforces required props.Fragment's props are exported asFragmentProps(as in React), so declarations emitted forcreateElement(Fragment, null)reference that type instead of inlining it.
Patch Changes
9b6c3a4: Include imported theme styles in server output when
$classor a class-map entry was captured at module scope or cached during an earlier render. Collect the theme and its dependencies when the class is rendered, preserving request-specific styles, dependency order, and CSP nonces.5f58b63: Production builds now report signal-engine, hydration, and DOM-binding errors as compact
Minified Octane error #<code>messages instead of shipping their full English text. Development messages are unchanged.1e4f1ee:
createElementnow acceptsnullas its config understrictNullChecks, matching React'sprops?: P | nullsignature. A barenullconfig infers props from the element type alone, and a non-null config still enforces required props.Fragment's props are exported asFragmentProps(as in React), so declarations emitted forcreateElement(Fragment, null)reference that type instead of inlining it.5a05767: Preserve and activate server-rendered content when a document shell hydrates a selected owner alongside portal siblings.
1bebd3b: Re-export
__methodDepfromoctane/universalandoctane/universal/nativeso compiler-emitted method-call dependency imports resolve on universal targets. Universal builds previously failed at module resolution while the default entry compiled fine.e813fc0: Prevent ReactCompat host elements from adding a layout box around React content.
211144a: Collect applied theme dependencies before a stylesheet first enters a server response, including when a component and its extending theme share a module. This preserves base-before-override CSS order and includes base-defined CSS variables for captured class-map entries.
5f58b63: Trim production runtime overhead: hydration root recovery no longer stringifies the root component or builds dev-only diagnostics in production builds; delegated events with no handler on their path leave the native event's
stopPropagation/currentTargetuntouched; and native-read modules no longer enter an empty read scope for empty@if/@elsearms.
0.6.0
25 Sep 20264b235d2: Add opt-in parser-time capture of native SSR form submissions. Forms naming a behavior owner can accept button, implicit Enter, and requestSubmit commands before client registration, preserving immutable accepted fields and submitter metadata for exactly-once behavior delivery. Behavior roots consume these commands with the optional captureFormSubmissions factory, keeping form routing out of ordinary behavior bundles. Unclaimed commands have a bounded lease, and forms without opt-in preserve native behavior.
6e86908: Report unsupported JSX spread children in client and server compilation and in editor diagnostics. Assigned style blocks now keep all selectors, including element and descendant rules that apply to elements carrying a class entry from the block.
b9b0bc4: Support
@{ … }blocks at JSX child position on universal renderers. Setup-bearing blocks now compile to a newuniversalBlockdescriptor that materializes inside a persistent child scope at the block's sibling position, matching the DOMchildSlotlowering: hook state survives parent re-renders and effect cleanup runs when the scope is discarded. Empty blocks are dropped and render-only blocks merge into the parent template, and the explicit{() => @{ … }}scoped-child spelling lowers to the same form.@{ … }inside@if/@for/@switch/@trybodies — previously emitted as a raw statement that failed printing — compiles through the same path.
Minor Changes
4b235d2: Add opt-in parser-time capture of native SSR form submissions. Forms naming a behavior owner can accept button, implicit Enter, and requestSubmit commands before client registration, preserving immutable accepted fields and submitter metadata for exactly-once behavior delivery. Behavior roots consume these commands with the optional captureFormSubmissions factory, keeping form routing out of ordinary behavior bundles. Unclaimed commands have a bounded lease, and forms without opt-in preserve native behavior.
6e86908: Report unsupported JSX spread children in client and server compilation and in editor diagnostics. Assigned style blocks now keep all selectors, including element and descendant rules that apply to elements carrying a class entry from the block.
b9b0bc4: Support
@{ … }blocks at JSX child position on universal renderers. Setup-bearing blocks now compile to a newuniversalBlockdescriptor that materializes inside a persistent child scope at the block's sibling position, matching the DOMchildSlotlowering: hook state survives parent re-renders and effect cleanup runs when the scope is discarded. Empty blocks are dropped and render-only blocks merge into the parent template, and the explicit{() => @{ … }}scoped-child spelling lowers to the same form.@{ … }inside@if/@for/@switch/@trybodies — previously emitted as a raw statement that failed printing — compiles through the same path.
Patch Changes
411c555: Match server and client hydration boundaries for render-function children in
descriptor hosts, preserving their DOM identity, refs, and events alongside
component siblings.ac2a217: Upgrade
@tsrx/oxcto 0.16.0 sooxlintparses a project's.oxlintrc.jsonwith the project's own Oxlint when that one is newer than the vendored pin. A rule added to Oxlint after the pin is no longer rejected as unknown.c3dda51: Fix universal-renderer compilation of JSX used as a value in expression position — a component prop (
card={<Card/>}), a sole expression child, or a renderable hole. The universal expression rewriter lowered only nested template nodes, so a root JSX expression passed through to DOM codegen and emitted descriptor-runtime helpers (createScopedValue,createElementFromConfig) that universal entries such asoctane/universal/nativedo not export, breaking native bundles. Root JSX now lowers to the sameuniversalValue/universalComponentrepresentation as nested JSX.
- 0.5.x
0.5.0
24 Sep 2026752d750: Allow custom SSR hosts to share an immutable initial document signal seed with root, deferred, independent and streamed hydration. Matching native historical reads use versioned manifest references, while differing histories retain their own entries and live writes remain authoritative after adoption commits.
3390f40: Build newly visible fragment content inside an empty server hydration range without adopting its closing marker, preserving surrounding server nodes and mismatch diagnostics. Preserve immutable component framing across split Hydrate captures, retain separate server frames for mutable or shadowed component bindings, and build replacement component bodies as client DOM. Keep reassigned component declarations writable and decline callsite assumptions about their original bodies.
d285bd7: Diagnose actual eager imported signal
.get()reads when activating compiled DOM
bindings instead of silently presenting an unsubscribed snapshot. Pass a signal
handle directly for a live binding, or provide a deliberate sample through the
BindingSource snapshot. Props-based sampling, pure foreign.get()methods and
already subscribed native attribute projections keep their existing behavior;
ordinary SSR reads are unchanged.
Minor Changes
752d750: Allow custom SSR hosts to share an immutable initial document signal seed with root, deferred, independent and streamed hydration. Matching native historical reads use versioned manifest references, while differing histories retain their own entries and live writes remain authoritative after adoption commits.
Patch Changes
3390f40: Build newly visible fragment content inside an empty server hydration range without adopting its closing marker, preserving surrounding server nodes and mismatch diagnostics. Preserve immutable component framing across split Hydrate captures, retain separate server frames for mutable or shadowed component bindings, and build replacement component bodies as client DOM. Keep reassigned component declarations writable and decline callsite assumptions about their original bodies.
d285bd7: Diagnose actual eager imported signal
.get()reads when activating compiled DOM
bindings instead of silently presenting an unsubscribed snapshot. Pass a signal
handle directly for a live binding, or provide a deliberate sample through the
BindingSource snapshot. Props-based sampling, pure foreign.get()methods and
already subscribed native attribute projections keep their existing behavior;
ordinary SSR reads are unchanged.701b8c3: Allocate a signal scope's request, resource, streamed-result, derived-binding,
adoption and trace bookkeeping only when those features are used. AuseSignal$
hook scope now retains about 70% less memory, and bundles that use scoped
signals without streamed results are about 700 bytes smaller after gzip.
Streamed selections move into a capability that only the stream ingress
functions create.4f25786: Allow native host binding leases to hand off known unbound provider spreads while preserving renderer ownership of external styles and children.
afc0bee: Compile fixed-node
'use dom bindings'views whose channels are all attributes,
booleans, ARIA/data attributes, classes or text to a scalar-only adopter. It keeps
the same claims, signal handles, transition presentation and cleanup, but no
longer ships the projection, control, class-group, style-restoration, host-handoff
or URL-sanitization code these views never use. The documentedadoptBindings
example is about 41% smaller after gzip. Views with other channels keep the
general adopter.2d46905: Update the shared TSRX compiler dependency to
@tsrx/core0.3.2. Each@switch
arm is now its own block scope, so two arms can declare the same local, and@importinside a<style>block is now thetsrx-css-importcompile error.octane/tsrx-iterablealso re-exportsmap_iterable_async, which core now uses
in the editor for a@forwhose body awaits, so the loop binding keeps its type.
Type inspection claims only that loop's@forkeyword.a95c2dc: Keep
.tsrxmodules that only declare signals renderer-free, as plain.ts
modules already were. They previously activated the renderer's native-read
driver on load, so a renderer-free consumer such asadoptBindingsbundled the
renderer (about 24 KB gzip in a measured example). Modules that render signal
reads still activate native reads themselves through their documentedoctane/signalsimport; a component that reads signals during render without
that import no longer relies on the declaring module to activate them.fd81578: Return
@else ifbranch values on universal renderers. An@else ifarm
parses to anIfStatementalternate, which the universal compiler routed
through block codegen: the chained branch values were emitted as setup
statements and the else thunk returned an empty range, so every arm but the
first rendered nothing. The else thunk now returns the chaineduniversalIf
value, so@if/@else if/@elsechains — including chains nested inside
another arm — select their branch correctly.bdd7db7: Allow
@forwithout akeyclause on universal renderers.Universal lowering used to reject
@for (const x of xs)with "universal @for
ranges require an explicit key", forcing boilerplate on static or throwaway
lists. The universal runtime reconciles ranges by key only — there is no
unkeyed path — so the compiler now synthesizes a positional key
((item, index) => index) whenkeyis omitted, matching the implicit-index
semantics an unkeyed list has elsewhere.An explicit
keyis unchanged and still recommended for reorderable stateful
rows: with a positional key, item state (hooks, component owners, uncontrolled
leaf state) follows the slot rather than the item across reorders. The DOM
renderer is untouched — its unkeyed@forcontinues to fall back tox.id ?? x.47580bd: Keep a live Suspense replay alive across an unrelated scheduled render so a
settled boundary still reveals its content.A committed
@try/@pendingboundary wires its thrown thenables into a local
replay: on settle the root queues a microtask that re-renders with the pinned
memo cache. The next scheduled render unconditionally deactivated that replay
before preparing its own attempt. When the render only re-executed a scoped
owner, or retained the suspended subtree because its inputs looked unchanged,
nothing else ever re-attempted the region — the pending arm stayed on screen
after its promise had already resolved.preparenow hands a live non-transition replay's memo cache to the fresh
attempt and disables retention for it, so the render itself re-attempts the
suspended regions. A settled boundary resolves inside that commit; one still
waiting on its thenables re-suspends and republishes a fresh replay.
Transition replays keep their existing supersede semantics.
- 0.4.x
0.4.3
23 Sep 2026d9ed0f4: Load compiled
.tsand.tsxmodules with Bun's TypeScript loader inoctane/compiler/register.Compiled output for a
.tsor.tsxsource keeps the TypeScript constructs that
have runtime semantics, such asenum. Vite's TypeScript transform consumes the
same output. The Bun preload used to hand this output to Bun as plain
JavaScript, so a component module that declared anenumfailed with a syntax
error. It now keeps the source file's loader. Compiled.tsrxoutput is still
loaded as JavaScript.3fbe5b5: Specialize the common app-entry root shapes in production builds, not only
createRoot(target).render(App)in a plain.tsmodule.A root whose whole lifetime renders compiled
@{}components skips the
returned-value reconciler and its bundle graph. That proof now covers:A one-component Vite entry that renders
<App />drops from about 70 KB to
28 KB gzip, and a directhydrateRootentry from about 88 KB to 50 KB. On a
proven root,<App a={x} />with literal, function, or identifier attribute
values is passed to the root asAppplus its props, with livedefaultProps
applied as before. Keys, children, spreads, and other attribute expressions
keep the element path.ae7f970: Close four compiler soundness gaps in inferred proofs and generated code.
Patch Changes
d9ed0f4: Load compiled
.tsand.tsxmodules with Bun's TypeScript loader inoctane/compiler/register.Compiled output for a
.tsor.tsxsource keeps the TypeScript constructs that
have runtime semantics, such asenum. Vite's TypeScript transform consumes the
same output. The Bun preload used to hand this output to Bun as plain
JavaScript, so a component module that declared anenumfailed with a syntax
error. It now keeps the source file's loader. Compiled.tsrxoutput is still
loaded as JavaScript.3fbe5b5: Specialize the common app-entry root shapes in production builds, not only
createRoot(target).render(App)in a plain.tsmodule.A root whose whole lifetime renders compiled
@{}components skips the
returned-value reconciler and its bundle graph. That proof now covers:createRoot(el).render(<App />)andhydrateRoot(el, <App />)in.tsx
and.tsrxentries;the documented module-level
const root = createRoot(el); root.render(App)
whenrootis not exported; andhydrateRoot(el, App, props)in plain modules.
A one-component Vite entry that renders
<App />drops from about 70 KB to
28 KB gzip, and a directhydrateRootentry from about 88 KB to 50 KB. On a
proven root,<App a={x} />with literal, function, or identifier attribute
values is passed to the root asAppplus its props, with livedefaultProps
applied as before. Keys, children, spreads, and other attribute expressions
keep the element path.ae7f970: Close four compiler soundness gaps in inferred proofs and generated code.
Builtin text-conversion proofs (
{String(x) as string}and primitive locals)
now share one mutation check. A replacement ofString,Number,BigInt,
orDatereached through aletalias, a parameter,globalThis.globalThis,
or a returned global object no longer keeps the builtin proof, so SSR renders
the live value instead of throwing. Computed reads and application.set()
calls on ordinary receivers no longer decline primitive-local proofs.Independent
<Hydrate>boundaries reject captures initialized by
destructuring or parameter defaults (const { value = useRef(null) } = props)
and by function or class declarations withOCTANE_HYDRATE_INDEPENDENT_OWNER_CAPTURE, instead of serializing them as
JSON.Automatic
use()creation dependencies no longer read ambient globals that
appear only inside callbacks or behind guards the compiler cannot replay
(try/catch,switch (typeof x),'x' in globalThis,??). Nullable
locals read in those positions become optional reads (value?.prop). A
non-arrow callback'sargumentsno longer becomes a component dependency.Generated source origins are applied copy-on-write everywhere, so a scoped
<style>inside a reverse renderer region (<Html>in an object scene)
compiles the same way whether or not parser ASTs are frozen.
d9ed0f4: Throw a migration error in development when code reads
Context.Provider.Context.Providerwas removed in favor of rendering the context itself as the
provider (<Ctx value={…}>). The compiler already rejects<Ctx.Provider>whencreateContextis in the same module. When the context is imported from another
module, however, the client used to fail with "Element type is invalid … got:
undefined" and the server with "comp is not a function". Neither message
mentioned the migration.In development, reading
.Provideron a context fromoctaneoroctane/server
now throws[OCTANE_CONTEXT_PROVIDER] Context.Provider was removed. Render the context itself as the provider: <Context value={...}>...</Context>.The.Consumerwarning now shares the same development-only helper. Production
bundles are byte-identical, and.Providerthere is stillundefined.08d9a82: Stop rendering a dormant
Hydrateboundary's earlier captures in development. When a mounted parent changes a dormant boundary's captures or provided context before it activates, Octane now repairs the server HTML's attributes, class, style, and text without a hydration warning oronRecoverableError, in development and production. Before this, a legitimately changed text hole reported a recoverable text mismatch. Mismatches in a boundary that activates with unchanged captures are still reported.57072e7: Fix hydration of an empty
{x as string}text hole that shares its parent with other nodes, such as<p>{b as string}<After /></p>withb = ''. The server now emits a one-node<!---->stand-in for the empty hole, so later siblings are claimed at the right position. Previously the following component was rebuilt or duplicated, and in some shapes the whole root was left empty. Hydration recovery also no longer removes nodes outside the block being recovered, so a misaligned claim cannot blank its host.0df8be7: Keep keyed
@forrows live when they read module state or mutable globals.A row that reads a module
let/var, a reassigned module function, or a
host or application global such aslocationorwindowno longer takes the
PURE or DEP-PURE survivor skip. Neither item identity nor the deps tuple can
witness those reads, so an unchanged row stayed stale after the value changed.
For example,@if (location.pathname === item.href)never moved the active
row. Since #1213 this also affected rows whose only control flow is a
host-only@if. Rows reading moduleconsts, unreassigned functions, and
standard language globals such asMathkeep the fast path.bfbba3f: Independent
<Hydrate>widgets now activate onidle(),visible(), andmedia().Before this change only
load()and captured interactions activated an
independent island. A widget withwhen={idle()},visible(), ormedia(q)
compiled cleanly and server-rendered its strategy, but the island bootstrap
never installed a trigger, so the widget stayed inert until it was clicked.
Its effects, timers, and subscriptions never started.The server now writes each of these strategies' non-default parameters on the
independent boundary. The bootstrap installs the same idle callback,IntersectionObserver, ormatchMedialistener that ordinary boundaries use.
It removes a pending trigger on pause and dispose, and re-installs it on resume.condition()and a function-formwhenneed the lexical parent to re-evaluate
them, so they cannot drive an independent widget. The compiler rejects either
form when written directly (OCTANE_HYDRATE_INDEPENDENT_WHEN). An opaquewhen
value that resolves to either one makes server rendering throw instead of
shipping a widget that never activates.8a3a1e7: Adopt instance-keyed server signal state inside independent
<Hydrate>islands.The client island adapter recreates the server's Hydrate frame as a component
scope, and that scope added its own invocation segment to every descendant's
signal instance key. The server renders island children directly under the
island's root key, so a component inside<Hydrate independent>computed a
different key on each side. Instance-keyed server state, such as aquery$
result, was then never adopted: activation ran the browser producer again. The
frame now resolves to the island's root key, so client and server keys match
with or without an@tryaround the component.8620e76: Keep signal declarations inside keyed
@forrows independent during SSR, and match their row identities during hydration, edits, and reordering.4467583: Keep initially empty provider children anchored to the surviving hydration range so opening nested provider content after hydration does not throw
NotFoundError.b353a67: Fix memo skipping updates when an own prop matching an inherited Object.prototype value is replaced by another prop.
b27e99a: Keep same-module root, private Context and compiled
Hydratespecializations in production builds that configure renderer boundaries. Only a module that renders a boundary tag now falls back to the generic paths, instead of every module in the project.78da1e4: Preserve
__proto__shorthand data properties when specializing DOM binding
child programs with fixed primitive props. Adoption and mounting now retain the
authored text and attributes without introducing object-literal prototype setters.e4bb4e0: Let esbuild tree-shake unused
createContext,memo,lazy, andcreatePortal
results.Octane declares these factories
/* @__NO_SIDE_EFFECTS__ */. Rollup and Vite
apply that annotation to calls in other modules, but esbuild never does: it
decides tree-shaking per file. Under esbuild, an unused module-scopeconst Ctx = createContext(…)therefore counted as a side effect and pulled in
the whole client runtime through the context's provider body.The compiler now marks direct calls to these imports with
/* @__PURE__ */, the
call-site convention every bundler honors. It does this for compiled.tsrx/.tsxmodules and for plain.ts/.jsmodules, including modules with
no hooks. Server output still marks onlylazy(), because the servercreateContextregisters the context it creates. Calls through a local that
shadows the import are left alone.In the
@octanejs/ariaminimal-import bundle, which imports onlyuseSeparator, esbuild output drops from 197,180 to 8,667 bytes raw (63,197 to
3,349 gzip). Vite output is unchanged.3f43780: Avoid quadratic option copying when preparing controlled multi-select updates for View Transitions, while preserving native selection behavior and commit isolation.
Expose the controlled select projection benchmark through the MCP benchmark tool.
3249c9a: Reduce the cost of potential signal bindings in ordinary code. A prop-driven attribute such as
title={props.label}now passes its statically selected scalar writer to the binding, so production bundles no longer retain the generic attribute route with its form-control writers and DOM routing tables (−5.1 KB gzip for a one-component counter). Text and attribute bindings no longer retain the restored-textarea hydration helper. In modules that importoctane/signals, an object-literalstylewith scalar values is written directly and allocates its owning Block only once a signal handle appears, and components that evaluate no authored code skip the native-read bracket.1871491: Keep component-local SSR signals independent for distinct object, function, and symbol list keys, without coercing opaque reconciliation keys. Preserve their identity through retries, hydration, and row reordering.
ea4e8be: Release an idle streamed-signal upstream as soon as its response is abandoned.
createStreamedSignalResultFramesran as an async generator that awaited the
upstream iterator'snext(). Cancelling the generator queued itsreturn()
behind that pending read, so an idle but open source such as an LLM token
stream, WebSocket, or SSE feed was only returned once it produced another
value. A client disconnect, request abort, or inactivity timeout could leave
it open indefinitely. Every producer wait is now interruptible. The consumer'sreturn()and the request signal settle the pending wait immediately, and the
upstream'sreturn()runs then. Backpressure, the inactivity-timeout rules,
and the result frame grammar are unchanged.Automatic streamed signals now degrade per channel at the 256-live-channel
budget. Previously the 257th concurrently live attempt failed the entire
multiplexer and threw away every in-flight result. Now only the overflowing
attempt is refused: it is neither streamed nor announced, so the browser loads
it itself.93ddb5a: Preserve child nodes when unchanged trusted HTML is rendered after hydration or on the client, including fresh
trustHTML()wrappers.929f1a6: Compile TypeScript enums, value namespaces, import aliases, and class parameter
properties in.tsrxmodules to plain JavaScript.Vite does not run its TypeScript transform on
.tsrx, so the compiler's output
has to be JavaScript already. Before this change anenumor valuenamespace
was printed as written, andvite buildstopped with[PARSE_ERROR] Unexpected token. Aconstructor(private x)parameter property lost its assignment
without any error.These declarations now compile to the same JavaScript tsc produces for an
ES2022 target:Enums get numeric auto-increment, reverse mappings for non-string members,
constant folding and self-references,export enum, and declaration merging.
Aconst enumis emitted as a regular enum, which is whatpreserveConstEnums
and isolated-module builds do.Value namespaces cover exported variables, functions, classes, and nested
enums and namespaces, dotted names such asnamespace A.B, and merging across
blocks and with functions or classes.import X = A.Bbecomes a variable, and it is dropped when nothing reads it.Parameter properties are declared as class fields and assigned in the
constructor, aftersuper(…)in a derived class.
Abstract members, index signatures, and method overload signatures are no longer
printed into the output either.Some constructs have no ES-module equivalent and now fail with a compiler
diagnostic instead of a bundler parse error:export =,import x = require(…), a destructured export inside a namespace, and an enum member without
an initializer when the member before it is not a constant number.d57f1b4: Reuse committed universal Suspense primary ranges when retaining pending content, avoiding repeated root-wide searches while preserving host identity and visibility.
0.4.1
22 Sep 202636d5f5e: Initialize dynamic iframe sources before connecting the frame to the document, avoiding a transient sandbox warning for foreign URLs while preserving source updates.
3da41d5: Accept supported native lowercase HTML attribute names in development host-property diagnostics while preserving guidance for casing mistakes, framework-only props, and SVG attributes.
4ac71d2: Keep roots added by initially empty sibling slots inside conditional and switch arms so replacing an arm removes all of its content.
Patch Changes
36d5f5e: Initialize dynamic iframe sources before connecting the frame to the document, avoiding a transient sandbox warning for foreign URLs while preserving source updates.
3da41d5: Accept supported native lowercase HTML attribute names in development host-property diagnostics while preserving guidance for casing mistakes, framework-only props, and SVG attributes.
4ac71d2: Keep roots added by initially empty sibling slots inside conditional and switch arms so replacing an arm removes all of its content.
4efa520: Keep eligible intrinsic
@forrow roots on the native template path when an explicit rootkeysupplies the row key. This preserves keyed reordering, uncontrolled input state, and hydration adoption without the redundant descriptor boundary. Existing key precedence and component key boundaries remain unchanged.
0.4.0
21 Sep 202634e83ce: Recognize an
octane.sourcemanifest marker on linked and workspace packages. A package outsidenode_modulesthat receives Octane transitively from a shared toolkit can now declare"octane": { "source": true }instead of re-adding anoctaneversion range it does not own, purely as a compiler marker. Installed packages undernode_modulesstill require a declaredoctanedependency, and ownership stays explicit per package.3a859dd: Specialize DOM binding child programs for explicitly fixed primitive props so unused presentation branches do not ship. Caller propagation is opt-in through
domBindingFixedProps; default child requests remain generic and shared. Dynamic values, literal defaults, DOM adoption, and renderer hydration handoff retain their existing behavior.Expose the fixed-prop binding benchmark in the MCP benchmark tool alongside the unified runner.
620769b: Admit keyed
@foritem bodies whose only nested structure is host-only
conditional content to theforBlockPURE fast path: an@ifarm that renders
only host output (including a narrowly proven nested keyed@for) carries
nothing opaque, so with no parent captures the body is a pure function of the
item and unchanged-identity survivors skip re-render entirely. The admission
fails closed — a component tag inside the conditional, one hidden behind a
function boundary such as a memoizable call'st => <Tag />callback argument,
a render-time hazard like an assignment in the@iftest, or a live imported
member read keeps the body off the pure path.
Minor Changes
34e83ce: Recognize an
octane.sourcemanifest marker on linked and workspace packages. A package outsidenode_modulesthat receives Octane transitively from a shared toolkit can now declare"octane": { "source": true }instead of re-adding anoctaneversion range it does not own, purely as a compiler marker. Installed packages undernode_modulesstill require a declaredoctanedependency, and ownership stays explicit per package.
Patch Changes
3a859dd: Specialize DOM binding child programs for explicitly fixed primitive props so unused presentation branches do not ship. Caller propagation is opt-in through
domBindingFixedProps; default child requests remain generic and shared. Dynamic values, literal defaults, DOM adoption, and renderer hydration handoff retain their existing behavior.Expose the fixed-prop binding benchmark in the MCP benchmark tool alongside the unified runner.
620769b: Admit keyed
@foritem bodies whose only nested structure is host-only
conditional content to theforBlockPURE fast path: an@ifarm that renders
only host output (including a narrowly proven nested keyed@for) carries
nothing opaque, so with no parent captures the body is a pure function of the
item and unchanged-identity survivors skip re-render entirely. The admission
fails closed — a component tag inside the conditional, one hidden behind a
function boundary such as a memoizable call'st => <Tag />callback argument,
a render-time hazard like an assignment in the@iftest, or a live imported
member read keeps the body off the pure path.dbcabb2: Store signal-instance identity directly on scope and block fields instead of a
per-mountWeakMaprecord, so stamping a component's scope costs a handful of
stores rather than an allocation plus a map entry. Lite components resolve their
signal owner without allocating a per-mount closure when the owner is already
current, and dormant blocks — no registered effects, no root-transaction capture
— skip the render bookkeeping those subsystems only need once armed. Effect
Event payloads queued under a hidden Suspense boundary now park on the boundary
and publish at reveal, so a rolled-back render never installs a stale impl.92227f6: Add
createResizeObserverto deliver coalesced native resize notifications in a
separate task, with queued-entry cleanup on unobserve and disconnect. Ordinary
state updates keep their existing microtask batching.Use deferred observer callbacks in Base UI measurement components and Floating
UI's element-resize adapter so geometry-affecting updates can settle without
ResizeObserver delivery-loop warnings. These bindings now require Octane 0.3.7
or newer in the 0.3 release line. Initial synchronous measurement and positioning
remain available, and the global ResizeObserver constructor is unchanged.6580880: Preserve following server-rendered components and bind their events when a completed
@tryboundary hydrates, catches a client error, or waits for a client retry.db35ac1: Preserve pending native query streams when a parent rerenders with unchanged captured inputs, avoiding duplicate browser producers during streamed hydration. Changed inputs still replace the pending primary.
f209f7c: Preserve the root signal namespace when hydrating streamed Suspense arms, so queries adopt server results instead of starting duplicate browser producers.
a2c3e07: Replay captured independent-island interactions after the adopted primary commits, so controlled input adoption cannot restore text cleared by an early command. Pending or removed islands retain or cancel their captured interactions until their owning lifecycle can replay them safely.
a2c3e07: Keep query and derived computations attached to live signal inputs when their declarations are first resolved during historical hydration. Queries now reselect after later input changes without repeating a completed initial server request.
- 0.3.x
0.3.6
21 Sep 202628f31c3: Thread deferred native acceptance through suspense resume and hidden-reveal publication.
commitResumeInnerandattemptHiddenRevealInnerdiscarded the result ofacceptNativeCapture, so a capture whose acceptance the deferred-layout driver had staged still reachedspliceOffscreenCapturewithdeferredNativeAcceptance === false. That splices an unaccepted capture and throws "A native capture must be accepted before publication." out of a suspense-retry or reveal commit, abandoning every effect, ref attach, and store sync queued on that commit. Both sites now pass the flag the wayflushRootTransactionsalready did.be361b4: Preserve the full resolution when the Vite plugin rewrites a client runtime
request to its server counterpart during an SSR build.The hook returned only
resolved.id, soexternal: truewas dropped and
rolldown tried to bundle an external specifier such asoctane/signals/server
as a path relative to the importer, failing the build withUNLOADABLE_DEPENDENCY. Returning the resolution keepsexternalalong withmoduleSideEffectsandmeta.
0.3.4
20 Sep 202687f19a8: Clear a component's previous returned subtree when its next output is undefined, including optional ViewTransition children and lazy body handoffs, while preserving compiled imperative output.
e855d68: Fix focused DOM moves between parents in browsers without native
moveBefore, including staged commits. Cross-parent insertions no longer enter the sibling-rotation fallback or incorrectly skip appending a node, while same-parent reorders retain editing continuity.2a952b8: Keep deferred hydration dormant when an unchanged native signal refresh rechecks a false condition. Preserve native subscriptions and early activation for parent capture or provider-context updates.
0.3.3
20 Sep 2026138ec3c: Compare deferred hydration attribute diagnostics with the initial client captures while activating children with the latest values. Mounted parent updates no longer produce false development attribute mismatches, and genuine initial mismatches remain visible even when corrected before activation.
0.3.2
20 Sep 2026ec57015: Keep scalar binding caches coherent when hydration retains an early DOM binding's publication, so the next normal render can restore historical props after the early binding is released.
167ee71: Adopt eventless restored textarea values into writable signals during initial binding, hydration, and accepted early-control takeover while preserving newer model and native-edit precedence.
751d0de: Preserve native Undo/Redo grouping for controlled textarea edits by updating their existing baseline text node. Keep native form reset consistent with the current value for writable signals as well as ordinary controlled textareas.
Patch Changes
ec57015: Keep scalar binding caches coherent when hydration retains an early DOM binding's publication, so the next normal render can restore historical props after the early binding is released.
167ee71: Adopt eventless restored textarea values into writable signals during initial binding, hydration, and accepted early-control takeover while preserving newer model and native-edit precedence.
751d0de: Preserve native Undo/Redo grouping for controlled textarea edits by updating their existing baseline text node. Keep native form reset consistent with the current value for writable signals as well as ordinary controlled textareas.
0724b1f: Preserve live signal values when a
satisfies-wrapped assignment replaces a global conversion constructor before rendering.167dfe9: Give every hydration wire literal exactly one owning module. The
@forarm
markers, the presentation-binding comment prefixes, the deferred-boundary
attribute names, theuseIdspelling, the element-scoped<ViewTransition>
stylesheet, and the cross-realmSymbol.fortags were each re-typed in two to
five modules, so a change on one side of a hydration boundary could diverge from
the other without any test noticing. They now live inhydration-markers.js
(which stays off thedom-tables.jsgraph so the pre-root capture bundle can
share them),dom-binding-protocol.js,css.js, and a newruntime-tags.js;octane/constantsre-exports the same names and values as before.Adopting a presentation view whose range carries no view id is now rejected
outright instead of being compared against the string"[b;undefined;root".27a37b6: Avoid allocating temporary UTF-8 buffers when checking streamed signal injection byte limits on Node hosts. Preserve the same frame bytes, limits, ordering, and backpressure, with unchanged TextEncoder accounting on hosts without Buffer.
0.3.1
20 Sep 2026c49ccb6: Skip dispatch and transaction snapshots for unchanged compiled native callback data while preserving ownership refresh, staged publication, and rollback for changed callbacks and captures.
eda8994: Production DOM compilation uses a smaller deferred hydration body for compiler-generated template children with no authored fallback. Boundaries with
split={false}can omit generic returned-output and fallback rendering while retaining SSR adoption, interaction replay, suspension and cleanup. Descriptor children, spreads, explicit fallbacks, split loaders and development/HMR builds retain the general Hydrate path.a44c082: Preserve matching server-rendered list ranges when hydration resumes a pending lazy child. First-fill adoption now starts inside the list's retained range, and only first-fill adoption discards extra server items. Genuine list mismatches still report recoverable errors and remove the extra items.
Patch Changes
c49ccb6: Skip dispatch and transaction snapshots for unchanged compiled native callback data while preserving ownership refresh, staged publication, and rollback for changed callbacks and captures.
eda8994: Production DOM compilation uses a smaller deferred hydration body for compiler-generated template children with no authored fallback. Boundaries with
split={false}can omit generic returned-output and fallback rendering while retaining SSR adoption, interaction replay, suspension and cleanup. Descriptor children, spreads, explicit fallbacks, split loaders and development/HMR builds retain the general Hydrate path.a44c082: Preserve matching server-rendered list ranges when hydration resumes a pending lazy child. First-fill adoption now starts inside the list's retained range, and only first-fill adoption discards extra server items. Genuine list mismatches still report recoverable errors and remove the extra items.
b819a83: Defer server-side signal identity serialization until an actual handle is read. Ordinary object-keyed lists no longer coerce reconciliation keys merely because opaque output might contain a handle. Actual handles retain the same nested-list and component-key identities across SSR, hydration and reordering.
a52d50f: Avoid redundant signal value adapters for immutable local values whose initializers are proven to return JavaScript primitives. Keep native read tracking, controlled input markers, and the existing potential signal capability for models imported after mount.
805deef: Reduce production DOM bundles for private contexts whose complete usage is proven to stay in compiled template providers and canonical context reads. Both the context factory and provider call omit generic returned-element and descriptor-child rendering. Exported, escaped, reflected, aliased, and opaque contexts retain the callable context API and generic child support; provider identity, state, SSR adoption, and cleanup stay unchanged.
17970fe: Avoid rewriting unchanged native event authority while preserving ordered staged publication, rollback, retired invocations, and owner changes.
9291944: Skip repeated signal-binding policy and handle probes for stable scalar attributes. Continue evaluating authored expressions, reconciling undefined attributes during hydration, reading signal handles, and restoring controlled inputs.
47d5d1d: Preserve the production compiler's closed Context lifetime proof through generated Hydrate capture slots, so private compiled providers can keep their existing void output path across code splitting. Exported contexts, authored capture overrides, opaque provider children, and unproven bindings retain the generic rendering path.
068ead5: Reuse the whole-style rollback snapshot for consecutive fixed-key declaration updates within one render checkpoint. Preserve separate hosts, intervening writes, CSS value coercion order, and abandoned or staged render restoration.
0b48d5d: Reduce production client bundles for compiler-extracted Hydrate templates without an authored fallback. Code-split boundaries reuse the compiled-child policy while preserving preload captures, native hydration, suspension, retry and cleanup. Authored overrides and opaque callers retain the general rendering path.
40ff19f: Initialize potential SSR signal identity recipes in the component frame's initial
object shape, while keeping ordinary frames free of optional identity fields.cc7a8c6: Release completed streamed signal channels after transport acknowledgement, count only live channels against the automatic stream limit, and drain ready channels without rescanning completed history. Reuse serialized frame byte counts without changing the wire format.
Treat streamed signal and NDJSON timeouts as inactivity limits rather than total response deadlines. Pause producer timeouts during backpressure and renew browser result timeouts on accepted progress, preserving cancellation, replay protection, and byte and mailbox limits.
Cancel abandoned injection producers when a streaming renderer fails or its consumer cancels, including producers paused while waiting for transport acknowledgement.
3350a41: Preserve live signal values when visible global-constructor mutations invalidate inferred inline conversion results, including wrapped and extracted native mutators.
0.3.0
19 Sep 2026a6d7f49: Remove the legacy
Context.Provideralias from client, server, and native contexts. Provide values with<Context value={value}>orcreateElement(Context, { value }, children)instead. The compiler rejects statically recognized legacy Provider access with migration guidance, and Octane bindings now use contexts directly. Binding peer ranges accept Octane 0.3 alongside their previously supported runtime lines.debd7df: Specialize nonescaping function-local const roots in production JavaScript and TypeScript entries when every render uses an imported compiled void component. Keep writable component exports on the generic rendering path so authored replacements can return ordinary renderable values.
873f4d2: Move native signal transition coordination behind the signals model capability so ordinary client applications no longer retain its preparation, retry, and publication policy. Preserve signals imported after an async Action awaits and atomic updates to consumers that receive signal handles through props.
The model entry now retains this coordinator, increasing standalone signals and signal-using SSR bundle sizes.
Minor Changes
a6d7f49: Remove the legacy
Context.Provideralias from client, server, and native contexts. Provide values with<Context value={value}>orcreateElement(Context, { value }, children)instead. The compiler rejects statically recognized legacy Provider access with migration guidance, and Octane bindings now use contexts directly. Binding peer ranges accept Octane 0.3 alongside their previously supported runtime lines.
Patch Changes
debd7df: Specialize nonescaping function-local const roots in production JavaScript and TypeScript entries when every render uses an imported compiled void component. Keep writable component exports on the generic rendering path so authored replacements can return ordinary renderable values.
873f4d2: Move native signal transition coordination behind the signals model capability so ordinary client applications no longer retain its preparation, retry, and publication policy. Preserve signals imported after an async Action awaits and atomic updates to consumers that receive signal handles through props.
The model entry now retains this coordinator, increasing standalone signals and signal-using SSR bundle sizes.
68a6690: Preserve explicit signal ownership when native event handlers are installed by components without signal bindings, including handlers adopted during hydration. Refresh ownership when bare or compiler-lifted handlers publish changed callbacks or captures, while keeping the committed authority if a suspended update is abandoned. Keep already queued native callbacks under their original authority when an earlier listener publishes a replacement. Avoid extra publication calls during ordinary mounting and updates that keep the same scope authority, and record rollback without per-handler undo closures.
c32e76b: Keep fixed scalar inline styles on their ordinary writers inside modules that use signals, while preserving reactive reads and native bindings for handles, spreads and accessors.
14fd908: Allow text and attribute signal bindings to omit unused form-control writer and
hydration adoption policies from production bundles. Keep the compiler helper
ABI, scalar caches, live subscriptions, and native control behavior unchanged.893cc83: Specialize production function-local roots created or hydrated with stable same-module compiled void components when their complete lifetime uses only proven void bodies. Preserve generic roots for escaping roots, writable component bindings, unknown initial or later render targets, development and alternate renderer modes.
- 0.2.x
0.2.16
18 Sep 20262049fa7: Keep native-only DOM presentation initialization separate from control and grouped-projection capabilities, so initializer-only compiled views do not retain unrelated host-operation helpers. Preserve initialization order, early edits, and older mount/adoption capability overrides.
80d5fb6: Preserve scoped CSS selectors in split Hydrate children when production builds compile mutable parser ASTs. Deferred and independent activation now inject the same scoped stylesheet that matches the server DOM.
3814f71: Preserve control-flow guards when inferring hook dependencies. Property reads behind a condition, an early return, or exception handling inspect own data values without invoking getters during render. Accessors and inherited properties track their receiver, while stable own fields and callbacks retain precise dependencies across fresh props and store snapshots. Optional receivers and guarded getters therefore retain their authored behavior.
Patch Changes
2049fa7: Keep native-only DOM presentation initialization separate from control and grouped-projection capabilities, so initializer-only compiled views do not retain unrelated host-operation helpers. Preserve initialization order, early edits, and older mount/adoption capability overrides.
80d5fb6: Preserve scoped CSS selectors in split Hydrate children when production builds compile mutable parser ASTs. Deferred and independent activation now inject the same scoped stylesheet that matches the server DOM.
3814f71: Preserve control-flow guards when inferring hook dependencies. Property reads behind a condition, an early return, or exception handling inspect own data values without invoking getters during render. Accessors and inherited properties track their receiver, while stable own fields and callbacks retain precise dependencies across fresh props and store snapshots. Optional receivers and guarded getters therefore retain their authored behavior.
b519670: Resolve independent Hydrate captures by their lexical binding and reject aliases or wrappers of parent-owned hook state. Unrelated same-name locals no longer reject valid standalone data or hide invalid captures; nested independent extraction also plans module moves before manifests are available.
ac02371: Avoid activating document-wide signal ownership for opaque scalar bindings and preserve proven primitive values through extracted JSX fragments. Register model Action frames from the signal graph so applications without signals can omit that implementation, while retaining late-loaded signals and atomic native presentation.
2049fa7: Expose
getLeadingHydrationListRangefromoctane/hydrationto resolve an owned SSR host's leading list through canonical wrapper ranges while preserving its binding receipt and rejecting incomplete or malformed boundaries.2049fa7: Diagnose reserved
on*attributes in native binding views as unsupported attributes rather than event handlers. Keep native camelCase event recognition, explicit unbound ownership and inline-event attribute safeguards unchanged.2049fa7: Allow scalar native-parent hydration handoffs to retain independently owned
data-*,aria-*, andtabIndexbindings alongside class and style. Keep opaque children and their controls outside the parent lease, preserve native attribute removal semantics, and retain the existing collision, cancellation, and host identity checks. Keep explicitly unbound lowercase native event handlers under normal renderer ownership.9b22cff: Keep explicit keys distinct from implicit positions and nested array paths in host-only children, preserving the correct input nodes and typed values when keyed children reorder.
02e0946: Activate independent Hydrate widgets using load() when their server sidecars are registered, and resume pending activation after a paused document becomes active again. Preserve the existing server DOM when independent widgets activate beneath parents that return JSX. Preserve the enclosing component's parallel use() warm plan when compiling nested templates.
358b5d4: Fix automatically inferred async creation dependencies to respect lexical scope and erased TypeScript syntax. Preserve safe evaluation of
typeofguards and their value reads for absent globals, and refresh requests when callback parameter defaults reference changed outer values.c988ad1: Preserve native Undo/Redo grouping when a writable textarea signal echoes an accepted native edit. Different programmatic values still update the textarea's reset baseline, and scalar or read-only values keep controlled-value mirroring.
0.2.15
18 Sep 202642a07b4: Preserve accepted scalar DOM-binding updates when hydrating older server-rendered state.
Compile scalar text leaves and mixed structural/scalar projections independently, retain the current bound text, attributes, classes, and style properties during hydration, and release their ownership when the binding is disposed or aborted. Unrelated DOM mutations still receive normal hydration diagnostics and repair.
0.2.14
18 Sep 202644bd89f: Allow a compiler-proven native host to transfer its class and known-provider style bindings to normal hydration while its opaque children retain their own rendering and control ownership. Keep the early layout active during suspension or refusal, preserve the current presentation during accepted transfer, and validate the host and its source before publishing the successor.
Keep the early scalar adapter's handoff symbol in the existing control registry so accessing the capability does not make cold renderer and event-lease helpers an eager dependency.
Retry skipped native effects through the existing native-read scheduler when their surviving render has already completed, without weakening stale-publication checks or reviving disposed owners. Preserve suspended-island ownership and held-transition priority.
Traverse lightweight DOM-context ancestors without treating them as hook-bearing blocks when locating a preserved hydration owner, so native effect retries remain safe while another island is suspended.
de270e3: Support explicit handoff of a standalone textarea signal value alongside an adopted native presentation. Hydration preserves live input and selection, transfers control ownership only at accepted publication, and keeps early bindings active when takeover is declined or suspended. Known-provider unbound style spreads retain direct compiled property bindings.
Keep early ownership intact when preparing a successor subscription fails, and avoid masking interrupted mounts with a secondary ref-cleanup error.
Release all prepared value successors when a later control invalidates presentation publication, preserving the original error and preventing stale input or model writers from being reclaimed by that root.
Reject competing presentation bindings for fields supplied by an unbound known-provider spread, including
class/classNamealiases.Type-check explicit scalar text intent against a signal handle's value in DOM templates, preserving direct bindings without application-side reads or unsafe casts.
Transfer direct scalar text signals through presentation hydration using the existing prepared binding lifecycle, including initially empty text ranges and live updates after acceptance.
44bd89f: Mark streamed signal selection and result scripts as renderer-owned transport so hydration can adopt the server HTML without reporting leftover protocol scripts as mismatched component output.
0.2.13
16 Sep 20265ead1ff: Allow
adoptBindings(element, View, source)to resolve an eligible composite view that returns another view through exact adjacent compiler-owned wrappers. Preserve native node identity and existing binding lifetimes while rejecting ambiguous, mismatched, or sibling-containing ranges.5ead1ff: Add owner-bound signal declarations, async derivations and keyed streams, direct native signal bindings, and independent hydration infrastructure. Add request-local server-call context, bounded streamed RPC, and explicitly batched independent reads. Preserve operation identity and cancellation boundaries across navigation and uncertain acknowledgements.
Allow a later widget activation to retry a failed framework-loaded stylesheet without discarding queued interactions or revealing the widget before CSS loads.
Support renderer-free global signal and streamed-state activation for hosts that retain server-owned HTML. Adopt initial document seeds before behavior reads, preserve early edits, bind native control properties without reconciliation, and let envelope owners emit the early capture script before interactive markup without duplicating it in rendered fragments.
Catalog the new core runtime diagnostics while preserving their error classes, and verify the published streaming bootstrap subpath and inline script export.
Keep individual and batched server calls on the page's origin when an authored base element points to another origin.
Keep reusable DOM, CSS, and component prop types scalar while allowing direct signal bindings at native JSX sites, preserving existing binding consumers. Use scalar public props for Zag's state-machine normalization results and to-print's imperative iframe options.
5ead1ff: Specialize renderer-free child bindings to explicit caller prop shapes, allowing a proven destructured rest parameter to forward supported native properties. Preserve ordered shape identity through imported binding requests and share normal SSR annotation allocation without restricting the ordinary component API. Unknown spreads and unsupported native ownership conflicts still fail clearly.
Allow proven conditional, child-view, slot, and primitive-text regions to transfer from early bindings to normal hydration without replacing their native nodes. Keep early interactions active through suspended or discarded attempts, validate the current presentation before publication, and retain explicit refusal for unsupported regions and writers.
Patch Changes
5ead1ff: Allow
adoptBindings(element, View, source)to resolve an eligible composite view that returns another view through exact adjacent compiler-owned wrappers. Preserve native node identity and existing binding lifetimes while rejecting ambiguous, mismatched, or sibling-containing ranges.5ead1ff: Add owner-bound signal declarations, async derivations and keyed streams, direct native signal bindings, and independent hydration infrastructure. Add request-local server-call context, bounded streamed RPC, and explicitly batched independent reads. Preserve operation identity and cancellation boundaries across navigation and uncertain acknowledgements.
Allow a later widget activation to retry a failed framework-loaded stylesheet without discarding queued interactions or revealing the widget before CSS loads.
Support renderer-free global signal and streamed-state activation for hosts that retain server-owned HTML. Adopt initial document seeds before behavior reads, preserve early edits, bind native control properties without reconciliation, and let envelope owners emit the early capture script before interactive markup without duplicating it in rendered fragments.
Catalog the new core runtime diagnostics while preserving their error classes, and verify the published streaming bootstrap subpath and inline script export.
Keep individual and batched server calls on the page's origin when an authored base element points to another origin.
Keep reusable DOM, CSS, and component prop types scalar while allowing direct signal bindings at native JSX sites, preserving existing binding consumers. Use scalar public props for Zag's state-machine normalization results and to-print's imperative iframe options.
5ead1ff: Specialize renderer-free child bindings to explicit caller prop shapes, allowing a proven destructured rest parameter to forward supported native properties. Preserve ordered shape identity through imported binding requests and share normal SSR annotation allocation without restricting the ordinary component API. Unknown spreads and unsupported native ownership conflicts still fail clearly.
Allow proven conditional, child-view, slot, and primitive-text regions to transfer from early bindings to normal hydration without replacing their native nodes. Keep early interactions active through suspended or discarded attempts, validate the current presentation before publication, and retain explicit refusal for unsupported regions and writers.
5ead1ff: Support flat destructured props, aliases, primitive literal defaults, and rest bindings in renderer-free authored views. Prepare parameter bindings once per snapshot so projection, event, and ref reads preserve JavaScript destructuring semantics. Keep unsupported patterns and arbitrary native spreads explicit errors.
5ead1ff: Allow renderer-free authored views to use canonical signal-handle checks, bounded native value projections, and named native event and ref callbacks. Keep callback bodies deferred until their native lifecycle, preserve committed captures and stable ref attachment, and allow explicit ref-prop forwarding through child views.
5ead1ff: Extend compiler-owned native presentation to authored text, conditional content, keyed lists, pure child views, and explicit mounting without loading the renderer. Preserve SSR identity, early native controls, focus and composition, and native ref lifetimes. Add fixed-shape imported attribute factory contracts so style adapters can preserve one ordered merge without repeated spread evaluation. Connect direct signal-valued native channels to the existing signal graph, without rerunning unrelated presentation projections.
5ead1ff: Move explicit declaration keys to trailing options:
signal$(initial, { key }),derived$(compute, { key }), andquery$(select, load, { key }). This replaces their positional authored-key overloads; update existing callers when adopting this beta API change. Compiler-generated identities and explicitcreateScopemethods retain their existing ownership behavior.5ead1ff: Keep transition orchestration out of early signal and control module dependencies so split-chunk builds can defer it with the renderer. Native input and transition behavior are unchanged.
5ead1ff: Allow explicitly adopted, compiler-proven fixed native views to transfer their early DOM bindings to
hydrateRootthroughbindingLeases. Keep early presentation and native commands active while hydration is pending, publish current values before refs, and retire old ownership without replaying already handled commands. Native updates beneath a suspended hydration boundary now retain that boundary's pending capture instead of publishing a child independently.The early entry remains renderer-free; normal hydration still requires explicitly loading the renderer. Structural regions, dynamic text, writable controls, and unsupported writers are not eligible for this optional handoff.
Retire displaced early bindings only after staged DOM publication, suppress refs from hydration attempts that never commit, and support fixed native views using fresh array/object class values. Hydration-lease errors use the standard production error-code catalog.
5ead1ff: Preserve event-time command payloads through an optional synchronous behavior capture hook, and support authoritative revision comparison for optimistic action receipts so older successful responses settle without replacing newer authority.
5ead1ff: Reconcile explicitly undefined native attributes when hydrating existing server markup. Direct attributes and native prop spreads now remove stale SSR values on the first client render instead of treating an empty client cache as an unchanged value. Preserve the adopted node and unrelated server attributes.
5ead1ff: Keep early whole-style bindings from sharing a module with renderer-only attribute and namespace tables. CSS serialization, units, caches, and existing helper exports remain unchanged.
5ead1ff: Allow trusted
knownAttributeSpreadscontracts to opt into signal-aware style objects withstyle: 'object'. Renderer-free bindings reuse the existing style capability, while contracts without this option retain CSS-text behavior.5ead1ff: Keep query implementation out of signal-only owner bundles and remove the legacy
scope.asyncSignal$method. Explicit-owner callers now importcreateResource(scope, key, describe); nativequery$declarations are unchanged.createScoperemains optional, and synchronous signals, draft edit receipts, request isolation, streaming ownership and historical adoption retain their contracts.Discard compiler-proven unused signal declarations without dropping initializer effects or diagnostics, and reduce repeated plain-data conversions when accepting SSR signal seeds. No runtime capability loader or extra initialization phase is added.
Add
bootstrapStreamedSignalResultsfor hosts that accept streamed signal results while owning their HTML placement. It shares the full receiver's authority, bounded delivery and lifecycle handling without retaining DOM placement code. The existingbootstrapStreamedSignalHydrationand region-registration API remain available unchanged.5ead1ff: Reduce renderer-free signal startup dependencies by separating native-control capture from optional island activation and keeping server stream observation mirrors out of the browser request engine. Preserve early input, stream cancellation, and per-consumer backpressure without changing author-facing APIs.
5ead1ff: Preserve grouped native
sxprojections through nested local components in renderer-free bindings, including reactive updates, keyed rows, and cleanup.5ead1ff: Add opt-in native
sxauthoring with signal-aware StyleX arguments. Normal rendering and renderer-free bindings share a native projection that prepares class, style, and metadata together, preserves SSR adoption and source ownership, and skips unchanged DOM writes. StyleX remains responsible for style composition, units, and extracted CSS.Expose a shared StyleX compiler contract and a TSRX type-check provider so supported native
sxexpressions can sample signal arguments without widening ordinary StyleX function parameters or component props. Forward native attribute contracts through the application Vite plugin.5ead1ff: Open failed server signal result channels before sending their sanitized error, preserving query error adoption when a promise rejects or an iterator cannot be constructed.
5ead1ff: Select keyed-list support only for renderer-free binding programs that can use it, including inactive branches, imported children, and caller-owned slots. Preserve older compiled descriptors through compatible runtime entry points.
5ead1ff: Start compiler-proven independent
query$andderived$reads together in complete static native JSX output, as well as adjacent local declarations. Preserve lazy declarations, branch reachability, original error and suspension boundaries, and ordered handling of opaque values. Unknown receivers, mixed text/renderable holes, dynamic host behavior, and resource-loading elements remain conservative boundaries.5ead1ff: Start compiler-proven independent query and asynchronous derivation reads together while retaining strict read order, actual data dependencies, cancellation, and lazy conditional work. This does not add transactional signal publication or delay native input updates.
Avoid speculative signal owners and eagerly serialized invocation paths in ordinary rendering. Preserve late signal activation and retired event ownership, and align keyed component identities between server rendering and hydration.
Retain pending component queries across rendering retries, releasing obsolete work on replacement, cancellation, and unmount. Support checked dynamic projection functions in immutable imported-factory configurations and imported string-token style keys in renderer-free views, preserving ordered attribute merges and property ownership.
Release native control leases when their exact signal owner retires, independent of application page-cleanup order. Initial dead-owner reads and genuine computation errors still fail; ordinary signal subscribers retain their final invalidation.
5ead1ff: Select host-spread hydration preparation only for components that use it, allowing simpler early-binding handoff components to omit generic spread and form machinery. Preserve the same adoption proofs, native ownership, and normal renderer behavior.
5ead1ff: Preserve streamed query delivery when a query waits for another query before starting. Dependent streams remain attached to the original SSR response and hydration adopts their delivered results without starting duplicate requests.
Preserve pending dependency snapshots in implicit derived signals and carry dependency refresh and stream-completion activity through async derivations.
Reuse the empty SSR list-key context between components while preserving copied keyed paths and request-local signal identity.
5ead1ff: Stage native signal writes made in transitions until their affected renderer and renderer-free presentations are ready. Keep committed values and public notifications unchanged while a query or derived result is pending, preserve urgent edits, and transfer accepted producer and binding subscriptions without restarting them. Reuse the existing transition journals and optional visibility driver for pending cues, timeout fallbacks, and cleanup.
5ead1ff: Complete renderer-free authored controls and whole/spread styles using the existing signal ownership and native style protocols. Preserve sampled one-way values, early edits, keyed controls, radio input ordering, CSS declaration order, and cleanup. Keep binding-only activation modules free of the renderer, accept the BindingSource callback contract, and support conservative chained string projections.
Preserve unchanged scalar text during native rendering and ViewTransitions by retaining its compiler-owned raw-value cache. This keeps text selection intact and avoids activating unchanged nested transition scopes without hiding signal pending/error recovery.
5ead1ff: Reuse the resolved server signal owner when invoking a component, avoiding a duplicate instance lookup while preserving nested request ownership.
5ead1ff: Let renderer-free structural bindings omit control, grouped-style/class, and native-initialization orchestration when their compiled view cannot use it. Preserve existing behavior for views that need these features and for older compiled descriptors.
5ead1ff: Select the compiled DOM binding adopter directly and share identical local child plans, reducing renderer-free startup code without changing view authoring or ownership.
5ead1ff: Share eligible compiled StyleX recipe constants between normal components and renderer-free binding artifacts in production browser builds. Preserve local StyleX optimization, style precedence, extracted CSS, and authored source maps. Expose the build-time sharing plugin for custom compiler adapters; leave development, server, and unsupported definitions on their existing paths.
5ead1ff: Export signal-handle predicates directly from their lightweight protocol module so a capability check alone does not retain signal-owner initialization or the signal engine. Predicate behavior and identity are unchanged.
5ead1ff: Derive direct signal binding and writable-control identities from authored source positions rather than generated component helper names. This fixes server/client identity mismatches for controls nested in conditional fragments, loops, and switch branches. Rebuild and deploy matching server and client output together because the compiler site-identity version changes.
5ead1ff: Preserve global signal SSR identities and activation metadata in plain modules that also use memo hooks when production inline memo optimization is enabled.
5ead1ff: Add compiler-backed adoption of fixed server-rendered DOM views through
adoptBindingsfromoctane/behavior. Opted-in views synchronously project an owned snapshot onto existing native elements without loading the renderer, replacing nodes, or taking over application event handlers. Unsupported structural authoring fails explicitly.5ead1ff: Let compiler-proven scalar signal derivations omit general async computation machinery. Avoid repeated owner resolution on cached signal reads while preserving request isolation, historical reads, and retirement checks.
0.2.12
16 Sep 2026ede01de: Accept native signal handles in DOM styles, including individual CSS properties and whole style values. Direct template styles update without rerunning component setup, and preserve signal cleanup, Suspense, server rendering, and hydration. Export
SignalCSSPropertiesfor signal-aware style objects while keepingCSSPropertiescompatible with ordinary CSS consumers.Keep binding CSS compatibility aliases pointed at plain
CSSPropertieswhen their layout helpers consume ordinary CSS values.Expose the signal style regression benchmark through the MCP benchmark tool.
248af4e: Avoid redundant hydration lookups when updating existing conditional and switch branches.
cece195: Reuse unchanged populated SSR replay snapshots and pending streaming settlement recorders across retry waves. Preserve metadata rollback, promise identity, cancellation, and request cleanup. Add the SSR replay and streaming benchmark suite to repository automation.
Patch Changes
ede01de: Accept native signal handles in DOM styles, including individual CSS properties and whole style values. Direct template styles update without rerunning component setup, and preserve signal cleanup, Suspense, server rendering, and hydration. Export
SignalCSSPropertiesfor signal-aware style objects while keepingCSSPropertiescompatible with ordinary CSS consumers.Keep binding CSS compatibility aliases pointed at plain
CSSPropertieswhen their layout helpers consume ordinary CSS values.Expose the signal style regression benchmark through the MCP benchmark tool.
248af4e: Avoid redundant hydration lookups when updating existing conditional and switch branches.
cece195: Reuse unchanged populated SSR replay snapshots and pending streaming settlement recorders across retry waves. Preserve metadata rollback, promise identity, cancellation, and request cleanup. Add the SSR replay and streaming benchmark suite to repository automation.
03dacb7: Stamp each block's context-dependency maps with the context epoch they were recorded or verified at, so memo and implicit bailouts skip per-entry version scans whenever no provider has committed a change; stale restamps and pending propagations still force the scans, preserving refresh behavior.
7d4dc4f: Reduce universal renderer prop-shape churn, materialization allocations, repeated feature scans, and unnecessary compact-list traversal while preserving keyed identity, transactional callbacks, and transport contracts. Expose the universal measurement suites through MCP.
277c10c: Compile fixed trailing style properties after leading object spreads into guarded per-property updates. Preserve spread evaluation, overrides, removals, and hydration, with complete object diffing when a spread preinserts a trailing key.
1198cdc: Skip the per-row
updateSurvivorcall in keyed reconciliation when a compiler-pure list row is provably unchanged — same item reference, same body, same position — so a stable keyed update no longer pays the survivor-update machinery for every no-op row. Moved, added, removed, index-shifted, non-pure, and de-opt rows still take the full survivor path, preserving render, journal, and rollback behavior.Preserve
import.metaandnew.targetsyntax when collecting dependencies for memoizeduse()arguments and server-rendered component props.Reduce repeated runtime work on the client and server. Empty descriptor hosts skip child-list scratch arrays, passive-effect batches reuse their scheduling callback, and identical server styles reuse their records and replay snapshots.
Expose the runtime-style-dedup, empty-host-children, and passive-scheduling benchmark suites through the MCP benchmark tool.
fe1b2b7: Avoid rebuilding complete style objects on repeated spread-key collisions. Preserve inherited setters, read-only properties, and transitions back to per-property updates.
733c98d: Key SSR scoped child-segment and occurrence counters by the frame-relative scope suffix instead of the full
ASYNC_SCOPEpath. Every component child re-scanned the shared path prefix during counter lookup, so SSR render cost grew with tree depth — measured ~53% faster on an arm-heavy SSR workload and ~16% faster on a plain nested-component tree, with byte-identical rendered output. Async identity, arm segment numbering,use()occurrence keys, replay, streaming, and hydration seed behavior are unchanged.13604b9: Avoid temporary boundary-collection copies during streaming SSR completion, error and abort scans, and reuse immutable CSS/head snapshots for completed boundaries. Extend the benchmark catalog with the final SSR and client coverage investigations from the runtime performance audit.
527358c: Complete the remaining Strong compiler checks for fetch-driven effects, effect chains, prop-derived initial state, explicit and null dependencies, manual memo hooks, JSX list mapping, index keys, suppression props, trusted HTML, and compatibility imports. Preserve equivalent dependency arrays as hints and report them without failing strict CLI analysis. Add compiler-owned declaration caching for Strong authoring, the
trustHTML/TrustedHTMLAPI, and nominal Strong JSX types while preserving compatibility modules.Strong opt-in intentionally changes generated code for eligible hook-input declarations: their identities are cached in development and production until inferred inputs change. It also normalizes proven built-in hook aliases and infers dependencies for unshadowed
undefinedplaceholders. This applies to both the directive and the globalstrong: trueoption. Ordinary callbacks and mutable values retain their authored evaluation and lifetime. The keyed@formigration applies to.tsrx; keyed JSX mapping remains supported in.tsx.CLI JSON reports include the hint count even when it is zero, and MDX diagnostic types represent errors, warnings, and hints.
The eager prop-state check covers both
useState(value)anduseReducer(reducer, value). A lazy state initializer or explicit third reducer initializer declares a deliberate initial capture. Subscription and timer callbacks keep their event-driven semantics and are excluded from effect-chain writes.bb11d0b: Upgrade to TSRX core 0.2 and the renamed native parser, @tsrx/oxc 0.13. Lazy destructuring (
&{ ... }and&[ ... ]) is no longer accepted by the compiler or editor tooling. Use ordinary object and array destructuring instead.Support property-specific kebab-case CSS names in
CSSProperties, including signal-backed HTML and SVG styles. Preserve Octane's numeric length support:width: 400still means400px. Runtime style handling is unchanged.777cef3: Align ViewTransition with React 19.3: fix activation classes, type maps, authored style restoration, mutation and layout detection, nested sharing, instance refs, and callback cleanup at animation finish. Forward native transition types, keep unanimated controls interactive, and wait for relevant resources and navigation. Animate streamed Suspense reveals with coordinated hydration and client updates.
Prepare ViewTransition renders with staged DOM commits so snapshot activation uses the finished boundary props while preserving existing node identity and committed lifecycle visibility.
Keep ordinary DOM operations on an inline native receiver path to avoid per-node staging helper calls when no ViewTransition is active.
Skip inactive staging calls during effect and scope cleanup, including Activity and Suspense deactivation after a ViewTransition has completed.
Add opt-in
scope="element"boundaries with local names and pseudo-element handles, independent sibling and nested animations, coordinated streamed reveals, and normal DOM commits when native element transitions are unavailable.Expose the ViewTransition bundle and native-work benchmark through the MCP benchmark tools.
0.2.11
14 Sep 2026fdb790a: Keep nested scoped JSX responsive to context changes, isolate hooks and memo caches across independently compiled render bodies, and invalidate stale output when lazy bodies change. Preserve component ownership across mixed compilation modes. Expose the production body-ownership benchmark through MCP.
1bc1926: Resolve the renderer-region owner with a single lookup at the top of the block chain instead of a WeakMap read per ancestor on every provider-less context read, preserving context defaults and foreign-renderer routing.
2789eab: Read descriptor list keys directly during reconciliation without creating a key callback for each list render, while preserving keyed identity and hydration behavior.
Keep mapped component slots compatible when rendering switches between native array mapping and a custom map implementation, preserving hydrated inputs and component identity.
Patch Changes
fdb790a: Keep nested scoped JSX responsive to context changes, isolate hooks and memo caches across independently compiled render bodies, and invalidate stale output when lazy bodies change. Preserve component ownership across mixed compilation modes. Expose the production body-ownership benchmark through MCP.
1bc1926: Resolve the renderer-region owner with a single lookup at the top of the block chain instead of a WeakMap read per ancestor on every provider-less context read, preserving context defaults and foreign-renderer routing.
2789eab: Read descriptor list keys directly during reconciliation without creating a key callback for each list render, while preserving keyed identity and hydration behavior.
Keep mapped component slots compatible when rendering switches between native array mapping and a custom map implementation, preserving hydrated inputs and component identity.
8e5ca22: Preserve accepted scoped descriptor children when Providers change host/component child shapes. Reuse known descriptor event names and reduce delegation arrays, child traversal, redundant persistent host writes, repeated form source resolution, and select option reads while preserving live DOM, event, and form-control behavior. Expose the descriptor-renderer benchmark suite through the MCP server.
fa11c10: Reuse wrapper-path serialization for nested explicitly keyed children while preserving key identity, hydration, and custom key conversion behavior.
ade5be8: Reduce compiler-generated handler, branch capture, and server rendering overhead while preserving event, branch, and SSR evaluation semantics.
Expose the compiler-output benchmark suite through the MCP benchmark tool.
1e12db7: Reduce hook path resolution, optional-argument handling, state getter lookups, and warm-plan bookkeeping. Reuse external-store subscription dependencies when the subscriber is unchanged. Add deterministic Hooks performance diagnostics to the benchmark catalog.
6284156: Reduce scheduler batch bookkeeping and skip ref sorting for sibling-only attachment queues, preserving update ordering, effect lifecycle checks, and render-loop limits. Expose deterministic scheduling benchmarks through the MCP benchmark catalog.
239dab0: Invalidate cached output when a retained Context Provider switches compiled child bodies, so returning to an earlier body renders its current content while preserving mounted DOM and hook state.
3c1cc55: Restore enumerable symbol values when a root render suspends and preserve keyed row state when an urgent update shares a batch with a suspended removal. Reduce row input and retirement bookkeeping, and reuse the live DOM value already read when journaling descriptor text updates. Expose the root transaction benchmark suite through the MCP server.
23b6a75: Speed up server rendering by picking the escape pre-scan by string length in
escapeHtml: a stateless non-global regexp test for short strings, threeindexOfscans for long ones.The previous global regexp paid
lastIndexbookkeeping on every call; the length split keeps the cheaper scan in each regime. ~20% faster median render on the 500-card SSR benchmark with byte-identical output.68d1ea1: Reduce per-node SSR bookkeeping cost in the emission hot path: frame-local scoped counters (per-arm child ordinals and per-site
use()occurrences) now live in a flat pair list — comparing scope strings directly instead of hashing them into aMap— and promote to aMaponly past eight distinct keys.process.env.NODE_ENVis sampled once per synchronous render pass rather than read on every attribute/style emission check, with public entry points still reading it directly so out-of-pass calls never see a stale sample.68d1ea1: Reduce per-component SSR bookkeeping allocation: replay snapshots now share immutable empty collections instead of copying empty
Map/Set/array state, stream boundary ancestor/owner key lists reuse a shared empty, andHookPasshook/occurrence maps are allocated lazily on first stateful or native hook call. Component-heavy server renders allocate roughly half the bookkeeping garbage they did before, with identical streamed output and unchanged render-phase replay semantics.58da344: Keep compiler memo caches off scope slot arrays, stabilize internal hook, host, list, and render-capture records, and initialize memo and template caches without sparse namespace entries. Preserve memo identity, staged hook values, keyed DOM reuse, and commit-time cleanup behavior.
8a45222: Reduce DOM attribute, spread-prop, template mounting, metadata, and delegated-event work while preserving hydration, rollback, native event descriptors, and custom-element connection behavior.
Expose the DOM attribute, template mount, and spread host benchmark suites through the MCP benchmark tool.