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.
- 0.2.x
0.2.8
11 Sep 20268a3da65: Avoid scheduling a capture fallback task for ordinary delegated discrete events when no controlled form restoration is pending. Preserve the fallback for a controlled edit whose native bubble is stopped below the root, including controls armed after capture and restores queued by nested events.
70c29ab: Avoid emitting a parallel
useBatchfor proven module-level context-only reads in TSRX and plain TypeScript. Mixed promise reads and child warming continue to batch as before.9b23f6c: Avoid building prefixed string keys for unkeyed top-level descriptor lists.
Patch Changes
8a3da65: Avoid scheduling a capture fallback task for ordinary delegated discrete events when no controlled form restoration is pending. Preserve the fallback for a controlled edit whose native bubble is stopped below the root, including controls armed after capture and restores queued by nested events.
70c29ab: Avoid emitting a parallel
useBatchfor proven module-level context-only reads in TSRX and plain TypeScript. Mixed promise reads and child warming continue to batch as before.9b23f6c: Avoid building prefixed string keys for unkeyed top-level descriptor lists.
afdc361: Reuse the encoded wrapper path for nested unkeyed descriptor siblings during client reconciliation.
7804852: Avoid temporary argument arrays when running effects without dependency arguments.
612b639: Update the shared TSRX compiler dependency to
@tsrx/core0.1.71.621147d: Avoid allocating a rest-argument array when rendering children inside a reused host component.
266302e: Keep the first resolved context provider inline on each consumer, allocating a provider cache Map only when that consumer reads a second distinct context. Continue reading provider values live through updates and hosted-root changes.
de3f2e6: Index universal owner drafts only when a render reads an earlier owner's hook or ref, preserving fast reads of the newest draft and the latest draft after retries. Expose the new universal draft lookup benchmark in the MCP suite catalog.
c31f629: Add opt-in
universalHostBindingfor local direct native roots. A subscribed source can update selected host properties in one accepted batch without rebuilding the component tree on each change.bf8568e: Require a runtime
octane/signalsimport to enable native signal reads in a module. Unrelated$-suffixed names no longer change DOM compilation or reject non-DOM renderers; components receiving signal handles or readers through props can importoctane/signalsdirectly.c6300cf: Avoid constructing a native event path during the capture observer when a single root has no portals, and reuse the path across the capture and emulated bubble queues of nonbubbling events.
c31f629: Release earlier effect hooks after accepted universal renderer updates. Repeated native renders with unchanged effect dependencies no longer retain the callback history until unmount.
9b17f4e: Reuse static children-only warm plans when registering compiled component descendants, avoiding a fresh empty batch and warm closure on each render while preserving the first pending descendant's fetch discovery.
593bf2e: Seed frequently polled DOM expando keys on native prototypes, including raw HTML ownership, de-opt descriptors, and uncontrolled value baselines.
4a5edff: Skip ancestor cache searches for memo values when a new render cannot match any prefetched cache, while preserving suspended retries and transition resource reuse.
d4bf228: Avoid serializing top-level unkeyed descriptor positions into JSON for server async identity.
3459196: Reuse nested implicit descriptor key prefixes during server rendering while preserving Suspense retry identities and hydration output.
f6a0b41: Place hoisted SSR metadata inside an authored head at a fragment root, and omit the SEO stray-owner diagnostic from production browser bundles.
3606d04: Keep distinct client and server
memo()wrappers on stable property shapes while preserving live defaults and static-hoisting behavior. Expose the memo wrapper shape benchmark in the MCP suite catalog.1c28da5: Keep scoped JSX element and value descriptors on stable property shapes while preserving deferred children, cloning, and server rendering behavior. Expose the scoped descriptor benchmark in the MCP suite catalog.
1298a69: Reject known ambient browser-state reads during Strong renders with
OCTANE_STRONG_RENDER_AMBIENT_READ, including browser handle aliases andglobalThisproperty reads outside known standard language builtins. Preserve shadowing, events, effects, external-store snapshot callbacks, and lazy state initialization, and document how to render subscribed snapshots safely across server and client.856febc: Reject render-time reads of reassigned module-scope
letandvarbindings in Strong modules with source-located diagnostics. Keep compatibility modules and event, effect, and deferred reads unchanged, and document the snapshot-safe alternative.236d4b5: Reject render-time reads of
useRef.currentin Strong modules with a source-located diagnostic, while retaining event and effect reads and compatibility-mode behavior. Document the rule in Octane's authoring guidance and MCP skill.a8f34fd: Reject render-time calls to known state getters from
useState,useReducer, anduseLinkedStatein Strong modules, with source-located diagnostics. Keep event, effect, deferred, and compatibility-mode calls legal and document the snapshot-safe render pattern.c3b0bea: Check Strong-mode
@switcharms in their own lexical scopes so arm-local shadows and functions receive accurate render diagnostics without leaking bindings into sibling arms. Treat instance class field initializers as deferred work when a class is defined during render.9fd4fd8: Avoid allocating a temporary array when resolving nested universal hook slots.
e8f6067: Avoid empty batch arrays when registering client warm plans for complex components.
0.2.7
9 Sep 202652ac66c: Avoid repeated ancestry scans when ordering effects and ref attachments from nearby components.
1158956: Skip repeated class writes for fresh array, object, and function values when their composed class string has not changed. Preserve class composition, hydration, and SVG attribute behavior.
7f3c096: Update the shadcn registry baseline to 4.21.0 and adopt cn 0.2.6 across the Base UI, Radix, and React Aria bindings. Add Base UI Select, Navigation Menu, and Scroll Area wrappers using the release's Nova styles and Base UI 1.8.0 primitives.
Hoist and deduplicate Base UI's scrollbar stylesheet. Preserve global CSS rules in compiled Float style resources so selectors remain active instead of being removed by scoped-style pruning.
Update React Aria Components to 1.20.0, React Aria to 3.51.0, and React Stately to 3.49.0. Add TokenField and PreviewTrigger, keyboard shortcut handling, context menus, and the coordinated accessibility and localization fixes.
Patch Changes
52ac66c: Avoid repeated ancestry scans when ordering effects and ref attachments from nearby components.
1158956: Skip repeated class writes for fresh array, object, and function values when their composed class string has not changed. Preserve class composition, hydration, and SVG attribute behavior.
7f3c096: Update the shadcn registry baseline to 4.21.0 and adopt cn 0.2.6 across the Base UI, Radix, and React Aria bindings. Add Base UI Select, Navigation Menu, and Scroll Area wrappers using the release's Nova styles and Base UI 1.8.0 primitives.
Hoist and deduplicate Base UI's scrollbar stylesheet. Preserve global CSS rules in compiled Float style resources so selectors remain active instead of being removed by scoped-style pruning.
Update React Aria Components to 1.20.0, React Aria to 3.51.0, and React Stately to 3.49.0. Add TokenField and PreviewTrigger, keyboard shortcut handling, context menus, and the coordinated accessibility and localization fixes.
12ea12a: Skip cycle-tracking allocation for primitive universal host values crossing a transport.
40f14c3: Build universal owner replay identities only when pending memos need them. Ordinary native renderer updates no longer allocate identity indexes and replay paths for every nested component.
890527f: Reduce the retained size of delegated event handler bundles by placing their private brand after the function and argument fields. Preserve handler updates and in-flight dispatch snapshots.
4d00715: Keep client blocks on a stable object shape across ordinary renders, Suspense, Activity, and fetch-tree warming by reserving optional fields when each block is constructed.
faae10e: Load captured values by index in hoisted client template helpers, avoiding array iteration on keyed item and conditional arm renders while preserving shared capture positions.
c9447c3: Avoid redundant emitted class-field definitions when constructing client blocks and scopes. Their existing constructors retain the same fields and property order while initializing them once.
d5de04f: Preserve keyed rows when a root render clears a list, mounts its
@emptyarm, refills the list, and then suspends. The empty arm now parks only its own DOM, so rollback keeps the original rows connected and reusable, including after a large owned-list clear.02ecadb: Recognize locally proven primitive string, number, and bigint DOM children while preserving explicit
as stringtext bindings. Add an optional TypeScript project proof for one-shot Vite, Rspack, and Rsbuild production builds, with matching server and hydration output.1186622: Restore direct listeners on hoisted head elements after a suspended transition rolls back, including aliased native event handlers.
31aff05: Reuse the keyed list sibling chain when unwinding an interrupted first fill.
d97637b: Reuse a root-bound resource handle factory across universal host prop codec calls.
f86fb48: Reuse universal event dispatchers across accepted updates to surviving listeners. Keep the latest handler and priority active, while retiring listeners when their event site is removed.
0ba4016: Reuse fresh universal owner draft collections during the first render pass instead of replacing them immediately. Expose the new owner-draft benchmark in the MCP benchmark suite catalog.
2c1ffaa: Reduce root rollback work for newly created binding bags and blocks, compact keyed-list snapshots, and avoid redundant resolved-transition effect walks. Preserve exact keyed DOM, cleanup timing, and retry behavior across nested suspension windows.
c92b932: Skip scanning server Suspense arms for ViewTransition markers when none were rendered.
ab32364: Avoid scanning buffered server fragments for a document head when hoisted metadata is present.
2ffcc71: Promote scoped signals to a stable API. Detect signal capabilities automatically in the compiler and remove the experimental
nativeReadsbuild option. Signal handles and helpers keep their$naming convention; local hooks, inferred memos, async resources, and DOM SSR/hydration work through the standard toolchain. Add the signals website guide and llms.txt reference.7fca4d3: Keep the server component frame namespace in the initial object shape, avoiding a later property transition while preserving HTML, SVG, and MathML parser context through renders and retries.
fbbb53d: Store one- and two-argument delegated callback captures directly on their handler bundles, avoiding a separate array per mounted handler.
0.2.5
6 Sep 20261846318: Require Octane 0.2.5 or newer for testing-library and the Base UI 1.8 binding. Published 0.2.4 does not export
isInActScope. Restore the rootuseMediaQueryexport and keep its options argument optional.1846318: Support library components that use transitive and method-based custom hooks, typed namespaces, and generic interfaces in Octane source. Preserve committed render-phase state when a render suspends, retain server DOM and hydration data while a resolved Suspense boundary waits for client data, and support portals into document fragments and shadow roots.
Make testing-library rendering and hydration settle native effects consistently, and accept the full Octane renderable input surface.
Batch nested
actcallbacks and testing-library rerenders within their outer callback. Await the complete promise queue before resolvingact, including with frozen timeout clocks, so asynchronous positioning updates settle before assertions. ExposeisInActScopefor testing helpers to preserve this batching.Render synchronous iterable template loops on the client and during hydration, including sets and generators, while preserving the array reconciliation path.
Run native event handlers outside component render scope when a DOM update synchronously dispatches an event, such as blur from disabling a focused input.
Complete finite layout-effect update cascades before publishing DOM mutations to observers, including scheduled updates and repeated measurements in one component.
Preserve optional method-hook chains, including skipped arguments, method receivers, and short-circuit boundaries in both compiler emission paths.
Retain resolved Suspense native data in the public SSR result as well as its boundary hydration payload. Retire four Floating UI expected failures now covered by passing upstream ref and positioning assertions.
Enforce the existing external-store snapshot stability contract during commit cascades. Uncached Zustand object selectors reach the update-depth guard; use
useShallowto cache their selected values.Require Octane 0.2.5 for the updated Base UI, Base UI Utils, shadcn, and testing-library packages so the compiler and
isInActScopeAPI are available. Preserve exact server catch-node adoption when an initially resolved Suspense arm contains a rejected resource.45f9761: Clear an owned keyed list of inert host rows with one DOM removal after the root render commits. Retain connected rows for suspension rollback and keep the existing per-row teardown for lists with effects, refs, nested scopes, or portals.
Patch Changes
1846318: Require Octane 0.2.5 or newer for testing-library and the Base UI 1.8 binding. Published 0.2.4 does not export
isInActScope. Restore the rootuseMediaQueryexport and keep its options argument optional.1846318: Support library components that use transitive and method-based custom hooks, typed namespaces, and generic interfaces in Octane source. Preserve committed render-phase state when a render suspends, retain server DOM and hydration data while a resolved Suspense boundary waits for client data, and support portals into document fragments and shadow roots.
Make testing-library rendering and hydration settle native effects consistently, and accept the full Octane renderable input surface.
Batch nested
actcallbacks and testing-library rerenders within their outer callback. Await the complete promise queue before resolvingact, including with frozen timeout clocks, so asynchronous positioning updates settle before assertions. ExposeisInActScopefor testing helpers to preserve this batching.Render synchronous iterable template loops on the client and during hydration, including sets and generators, while preserving the array reconciliation path.
Run native event handlers outside component render scope when a DOM update synchronously dispatches an event, such as blur from disabling a focused input.
Complete finite layout-effect update cascades before publishing DOM mutations to observers, including scheduled updates and repeated measurements in one component.
Preserve optional method-hook chains, including skipped arguments, method receivers, and short-circuit boundaries in both compiler emission paths.
Retain resolved Suspense native data in the public SSR result as well as its boundary hydration payload. Retire four Floating UI expected failures now covered by passing upstream ref and positioning assertions.
Enforce the existing external-store snapshot stability contract during commit cascades. Uncached Zustand object selectors reach the update-depth guard; use
useShallowto cache their selected values.Require Octane 0.2.5 for the updated Base UI, Base UI Utils, shadcn, and testing-library packages so the compiler and
isInActScopeAPI are available. Preserve exact server catch-node adoption when an initially resolved Suspense arm contains a rejected resource.45f9761: Clear an owned keyed list of inert host rows with one DOM removal after the root render commits. Retain connected rows for suspension rollback and keep the existing per-row teardown for lists with effects, refs, nested scopes, or portals.
7a2a2bd: Preserve scoped CSS classes on elements with spread attributes during production server rendering.
1846318: Adopt a streamed
@catcharm by transferring hydration ownership to the replacement catch block so a seeded factory rejection does not leave a duplicate error tree.
0.2.4
6 Sep 2026aec5373: Reevaluate Octane app configs safely across environment changes and concurrent builds. Correct form controls, hoisted head metadata, and descriptor-children lexical shadowing. Align Window and Day Picker bindings with Octane's types and native events, and honor falsy Redux server state.
1f19beb: Prevent production API errors and static-file symlinks from disclosing server details or files outside the built asset tree. Preserve injected HTML and settle streaming SSR when callbacks fail, and compile imported descriptor-children components correctly through Rspack and Rsbuild.
d38cd81: Allow a scoped
<style>inside a split<Hydrate>child when its whole lexical style scope — the block and the host elements it stamps — sits inside the boundary. Client and server keep the authored-position hash. A scope that straddles the boundary is stillOCTANE_HYDRATE_SPLIT_STYLE.
Patch Changes
aec5373: Reevaluate Octane app configs safely across environment changes and concurrent builds. Correct form controls, hoisted head metadata, and descriptor-children lexical shadowing. Align Window and Day Picker bindings with Octane's types and native events, and honor falsy Redux server state.
1f19beb: Prevent production API errors and static-file symlinks from disclosing server details or files outside the built asset tree. Preserve injected HTML and settle streaming SSR when callbacks fail, and compile imported descriptor-children components correctly through Rspack and Rsbuild.
d38cd81: Allow a scoped
<style>inside a split<Hydrate>child when its whole lexical style scope — the block and the host elements it stamps — sits inside the boundary. Client and server keep the authored-position hash. A scope that straddles the boundary is stillOCTANE_HYDRATE_SPLIT_STYLE.a059b46: Refresh an already-injected scoped stylesheet when
injectStyleis called again with the same hash and different CSS, so HMR updates take effect.a40dae1: Speed up production server rendering of elements with canonical attribute names by avoiding redundant attribute aggregation while preserving spread precedence, value coercion order, and rendered output.
d38cd81: Scope
<style>blocks to their siblings (RFC tsrx-org/RFCs#1): a block styles its siblings and everything below them — the children list of the element or fragment it is written in, including the fragment a nested@{ … }block, a control-flow branch body, or an assigned element/fragment template renders — sibling blocks share one hash and oneinjectStylecall, nested scopes get their own hash and CSS is injected in lexical order,<style>may sit beside the output node in a code block or directive body, assigned blocks lower anywhere a declaration is legal and expose$class, exported or applied blocks keep every selector,<style apply={theme} />stamps a theme's classes on a scope, and{style(expr)}resolves to the full scope chain.Amendment A1 to the scope model: a standalone
<style>block is a child of an element or a fragment and styles the items beside it and everything below them — never the element that contains it. Every children list that holds a block is a scope with its own hash; two blocks among the same children share one, and a block in an element's children no longer stamps that element or any ancestor. To style an element, make the block and the element fragment siblings (<><style>…</style><div>…</div></>), inside@{ … }and directive bodies too: a body holds exactly one output node, so a block beside the output node is the multiple-outputs parser error and a lone block as the output is the newtsrx-style-standalone-needs-fragmentdiagnostic. Raw CSS in<style>is TSRX template syntax: a standalone block outside every@{ … }or@if/@for/@switch/@trybody — a plain function returning JSX, an element assigned at module scope — is the newtsrx-style-standalone-outside-templatediagnostic, and plain TSX keeps its own rule, where<style>{css}</style>is an ordinary element the client and server emitters pass through untouched (the Node parser entry now retries that shape in the JavaScript parser when the native facade's CSS reader rejects it).applyon a standalone block reaches the same elements as its CSS, and a standalone block's selectors are pruned against those elements: a rule that reaches none of them — one aimed at the containing element, say — survives only as a/* (unused) … */comment, as it does in@tsrx/core. A value factory written in plain TSX keeps its scoped CSS in a render-only@{ <>…</> }inside the returned fragment; that block is transparent grouping in value position too, so the returned styled fragment stays a staticFragmentdescriptor rather than lowering to a compiled renderer.style(expr)resolves to the scope chain only where TSRX reads a class value — the expression of a JSX attribute value (thestyleattribute excepted) or of a template child hole, directly or nested in array/conditional/logical/template expressions there (nested in the class value of an element the chain is stamped on, the call yields its value alone and the stamp adds the chain once, so a composed class never carries a hash twice); astyle(...)call anywhere else (a statement, a declaration initializer, a call argument, a callback body, astyle={style(p)}value) is a user call and prints as authored.c6516c0: In opt-in Strong mode, add compiler errors when a built-in hook value and its dependent effect are declared outside the sole nested
@{…}block that uses them, or a named native event handler is declared outside the sole deeper block containing its direct event use. Permit parent-owned hooks in conditional and keyed arms, plus inline and same-scope named handlers, including shorthand event attributes. Report the source declaration and suggested block location in compiler and editor diagnostics.3988e85: Memoize unitless CSS property classification so numeric style writes skip repeated string allocation.
20e2c96: Keep a spread
classwhen a styled host adds its scope hash.A synthesized scope class now merges with a preceding spread's class instead of replacing it, so
props.classreaches the DOM on both client and SSR.eb32683: Fold
.tsxstring-literal expression children such as Prettier's{" "}into the client template, matching the server and.tsrxcompilers so hydration no longer duplicates the following element.3c5d2df: Update the TSRX compiler dependency to
@tsrx/core@0.1.67from tsrx-org/tsrx#74 and preserve matching sibling selectors (+and~) at the top of a style scope and inside branch fragments. Unmatched selectors remain pruned, and a scoped block still never styles its containing element.
0.2.3
4 Sep 20266135083: Keep the form-action submit handler, and the transition graph it starts, out of production bundles that never install a function form action, and emit the compiled setup checkpoint only for component setups that can schedule a render-phase self-update.
0705d6c: Reuse staged transition updates instead of repeating map lookups.
415e7fd: Recover server rendering throughput: a render whose hoisted head is empty no longer scans the whole response for a
</head>unless it is a document. Rendered output is unchanged.
Patch Changes
6135083: Keep the form-action submit handler, and the transition graph it starts, out of production bundles that never install a function form action, and emit the compiled setup checkpoint only for component setups that can schedule a render-phase self-update.
0705d6c: Reuse staged transition updates instead of repeating map lookups.
415e7fd: Recover server rendering throughput: a render whose hoisted head is empty no longer scans the whole response for a
</head>unless it is a document. Rendered output is unchanged.eff0271: Track
useTransitionpending ownership without allocating aSetper transition. A batch stores its starting hook in a field and the hook counts its pending batches, so a start → pending → settle cycle allocates the same three collections it did before the React behavioral audit fixes while keeping every corrected behavior: nested starts from another hook share the batch's pending window, a hook starting nested transitions inside its own Action is counted once, and a re-held batch becomes pending again.44d50db: Fix JSX ordering, whitespace, entities, keyed hosts and portals, parser-sensitive markup, document roots, and Suspense warming. Correct transition ownership, queued state updates, deferred values, effect cleanup and error delivery, native event dispatch, uncontrolled form defaults, form actions, and style updates. Preserve dispatch order across consecutive and awaited Actions, retain committed child inputs during urgent parent updates, and keep third-tuple getters available to bindings that supply their own hook slots. Preserve authored custom-hook arguments and symbol initial values through aliases. Keep manually slotted hooks independent when nested inside custom hooks. Preserve hoisting and remove unused manual hook providers from production bundles. Replay parent setup updates before initializing children, and explain missing document bodies after document-root hydration.
Escape application strings in every server renderer, accept renderable roots, recover buffered Suspense errors, and report hydration recoveries consistently. Add React-named migration types, StrictMode and batching pass-throughs, the useFormState alias, and server version exports. Document intentional differences in template children, branch identity, native events, scheduling, and SSR.
bf860be: Skip redundant HTML wrapping in compiled server loop bodies while preserving escaping, streaming, and hydration behavior.
0.2.2
2 Sep 20266a6dbc1: Fix controlled
value/checkededits being reverted before their handlers ran whenever anonXxxCapturehandler for the same event type was registered anywhere in the app. The browser runs a microtask checkpoint after every listener of an event it dispatches itself, so the capture segment's stopped-propagation fallback fired between the root's capture listener and its bubble listener and snapped every typed character back to the rendered value. Trusted events now close that window with a task, after native propagation has finished; script-dispatched events keep the microtask fallback.
0.2.1
2 Sep 202634ba45e: Align the delegated-event commit boundary with React's
batchedUpdates: the outermost dispatch of a discrete event now flushes synchronously only when a controlledvalue/checkedhost armed a state restore during that dispatch. Other handler updates stay in the microtask batch, so a script-dispatched event (dispatchEvent,click(),requestSubmit()) no longer publishes a commit mid-dispatch that React would publish after the dispatching script yields, and native listeners registered by other code observe the same pre-commit DOM under both renderers. Browser-dispatched events are unaffected in practice because the microtask checkpoint runs before the next native listener and the default action. Tests that asserted committed state immediately after a baredispatchEventshould wrap the dispatch inact()orflushSync(), as with React.Controlled
valueupdates now write the DOM property before syncing thevalueattribute, matching React'supdateInputorder. A control left non-dirty by a native form reset previously followed the attribute write and kept its non-dirty state; it is now marked dirty by the property write, so later attribute changes cannot drag the live value.
0.2.0
2 Sep 2026ddaa8c5: Promote Octane to beta and begin the 0.2 release line.
456ac4b: Preserve element and fragment ref ownership when a suspended root rolls back, so retries detach the previous ref and attach only the committed replacement. Keep native parsing first while accepting valid TSRX syntax supported by the JavaScript parser, without hiding operational failures or malformed input. Update TSRX core to 0.1.63 and adopt its released computed-key source mapping fix.
f6bea37: Keep ordinary delegated continuous-event updates responsive while an unrelated async transition Action is pending. Continuous events retain microtask batching, and updates explicitly wrapped in a transition still wait for the Action.
Minor Changes
ddaa8c5: Promote Octane to beta and begin the 0.2 release line.
Patch Changes
456ac4b: Preserve element and fragment ref ownership when a suspended root rolls back, so retries detach the previous ref and attach only the committed replacement. Keep native parsing first while accepting valid TSRX syntax supported by the JavaScript parser, without hiding operational failures or malformed input. Update TSRX core to 0.1.63 and adopt its released computed-key source mapping fix.
f6bea37: Keep ordinary delegated continuous-event updates responsive while an unrelated async transition Action is pending. Continuous events retain microtask batching, and updates explicitly wrapped in a transition still wait for the Action.
597929f: Scope delegated events to native root boundaries, preserve shadow/slot event paths and logical portal ancestry, and separate framework propagation cancellation from external native stop flags. Native
stopImmediatePropagation()no longer truncates an already-running delegated handler queue; usestopPropagation()as well to stop the remaining handlers in that logical phase.Expose native dialog lifecycle event handlers on logical ancestors in JSX typings.
- 0.1.x
0.1.51
1 Sep 20269321d39: Speed up hydrate module slicing in files with many split boundaries and private declarations.
fdb711a: Keep bounded scoped-signal trace retention constant-time after its history fills. Inspection still returns the latest events in chronological order as detached records.
5e80135: Reuse the local component Map for name lookups across TSRX renderer boundaries to speed compilation of large modules.
Patch Changes
9321d39: Speed up hydrate module slicing in files with many split boundaries and private declarations.
fdb711a: Keep bounded scoped-signal trace retention constant-time after its history fills. Inspection still returns the latest events in chronological order as detached records.
5e80135: Reuse the local component Map for name lookups across TSRX renderer boundaries to speed compilation of large modules.
ad499d0: Speed up large universal object-driver teardown batches by compacting detached sibling arrays once per transaction instead of shifting them for every host.
892da9a: Make hidden Activity caught-error publication scale linearly on reveal.
Activity now claims deferred reveal actions by their queue entry instead of searching and compacting the remaining array for every action. A production browser benchmark with 4,096 ordered
onCaughtErrorreports dropped from 9.62 ms to 2.92 ms while preserving hidden deferral, FIFO exactly-once publication, cancellation, retry safety, output identity, and clean unmount.babf8d7: Make hydration render-phase queue draining scale linearly.
Hydration now partitions the live scheduler queue in one pass instead of repeatedly rescanning and splicing it for every target-root update. A production multi-root benchmark at 1,024 rows dropped from 20.6 ms to 7.0 ms while preserving synchronous convergence, server-node adoption, foreign-root work, delegated interaction, render-loop limits, and error cleanup.
2785a2f: Allocate deferred-hydration procedural prefetch waiter sets only when
waitFor()is used.df82fbc: Speed up production void-component classification for long local memo alias chains.
0824502: Parse each authored TSRX module once for Vite's preflight classifications while keeping the compiler's authoritative parse and diagnostics unchanged.
47c8f54: Speed up compilation of large stable-hookful component graphs by propagating candidate invalidations, live captures, and private setter publications through dependency worklists instead of repeatedly rescanning every component.
0.1.50
30 Aug 2026157543f: Reuse normalized renderer configuration and compiled filename matchers across TSRX module classifications. Compiler integrations that retain normalized options no longer repeat renderer validation, signature serialization, brace expansion, and regular-expression construction for every source file.
4d13159: Make hydration of deeply nested, coextensive component wrappers scale linearly.
Hydration now remembers matching nested marker pairs for the lifetime of the adoption pass, resolves compacted range owners through a deferred parent chain, and removes contiguous redundant marker runs in one DOM mutation. A production SSR benchmark at 512 wrappers dropped from 39.2 ms to 4.9 ms while preserving server-node adoption, delegated interaction, logical marker multiplicity, and clean unmount behavior.
a944ff3: Make anchorless-safety propagation linear across deep same-module component graphs while preserving the emitted positional anchors and single-root classification.
Patch Changes
157543f: Reuse normalized renderer configuration and compiled filename matchers across TSRX module classifications. Compiler integrations that retain normalized options no longer repeat renderer validation, signature serialization, brace expansion, and regular-expression construction for every source file.
4d13159: Make hydration of deeply nested, coextensive component wrappers scale linearly.
Hydration now remembers matching nested marker pairs for the lifetime of the adoption pass, resolves compacted range owners through a deferred parent chain, and removes contiguous redundant marker runs in one DOM mutation. A production SSR benchmark at 512 wrappers dropped from 39.2 ms to 4.9 ms while preserving server-node adoption, delegated interaction, logical marker multiplicity, and clean unmount behavior.
a944ff3: Make anchorless-safety propagation linear across deep same-module component graphs while preserving the emitted positional anchors and single-root classification.
f9f0d23: Keep fallback collapsed-template handler updates linear in the number of native event sites by matching accepted listeners within each host's ordered event range. A 1,024-site update dropped from 2.4 ms to 0.5 ms while preserving host identity, atomic handler publication, nullable listeners, and teardown behavior.
edf2b9d: Speed up TSRX universal renderer validation by indexing authored source ranges before walking the AST. Validation diagnostics and compiled output are unchanged.
9779569: Export ReactCompat from octane/react to host real React components inside Octane templates. Preserve React state, refs, local boundaries, portals, and Activity lifetimes, map Octane context explicitly with bridgeReactContext, and support buffered server rendering with client hydration. Add a working ReactCompat playground example.
96c86fc: Reduce SSR latency for promises recreated by ancestor renders. Initialize the recreation guard from the actual first pending pass and immediately retry when switching to per-site replay, without waiting for an abandoned batch. Continue observing abandoned rejections and preserve dependency-waterfall, abort, and request-isolation behavior.
0.1.49
28 Aug 20268adc693: Add an opt-in experimental scoped signal engine backed by Alien Signals 3.2.0, with owned async resources, retained values, ready-state adoption, and native compiler read tracking. Expose the
nativeReadscompiler option through the application and bundler integrations while preserving explicit hook dependency arrays and the external Alien Signals binding.The experiment is not a stable API or a release recommendation. Local derived and async hooks remain deferred, and the accompanying evidence distinguishes supplemental compiler, runtime, and browser checks from the acceptance gates for the locked workspace.
Expose native read ownership and cached activity metadata through the existing DevTools inspector without evaluating signals or retaining a global graph registry. Match the private compiler ABI's CommonJS entry points to the public runtime so native SSR reads use one protocol instance.
Collect native reads around actual component invocation, including parameter defaults and indirect returns. Track and replay native reads in inferred memos, preserve deferred element inspection and rendering, and revoke live retained results when a contributing data owner retires. Keep held Suspense output, refs, effects, and native subscriptions together until replacement work is accepted.
Avoid duplicate native collection setup when invocation collection already owns the scope, while preserving independent child retirement, observer restoration, write guards, and stored-value witness replay.
Preserve nested Suspense ref lifetimes, finish caught deletion cleanup before replacement effects connect, and reveal the latest urgent state when it supersedes every held state update. Register native compiler and server hook diagnostics in the production error catalog and CLI explanations.
a51c8c6: Skip native text-change diagnostic AST analysis when authored TSRX cannot contain an input or textarea host.
0.1.48
27 Aug 20263ca30fc: Cache configured root membership and the sorted language-service root list in TypeScript-backed text inference so repeated warm snapshots no longer scale with unrelated project roots, and expose the regression benchmark through the MCP benchmark runner.
efdc8cb: Index component references once when compiling component-heavy modules.
Component declaration lowering now preserves the same client and server hoisting semantics without repeatedly sanitizing and scanning every growing source prefix.
922df8c: Skip manifest-cache scans for ordinary watched source changes while preserving package-manifest, full-reset, and diagnostic invalidation behavior. Expose the accompanying manifest-cache invalidation benchmark through the Octane MCP benchmark tool.
Patch Changes
3ca30fc: Cache configured root membership and the sorted language-service root list in TypeScript-backed text inference so repeated warm snapshots no longer scale with unrelated project roots, and expose the regression benchmark through the MCP benchmark runner.
efdc8cb: Index component references once when compiling component-heavy modules.
Component declaration lowering now preserves the same client and server hoisting semantics without repeatedly sanitizing and scanning every growing source prefix.
922df8c: Skip manifest-cache scans for ordinary watched source changes while preserving package-manifest, full-reset, and diagnostic invalidation behavior. Expose the accompanying manifest-cache invalidation benchmark through the Octane MCP benchmark tool.
8a8afd8: Cache shared ancestry while ordering batched component updates so deeply nested render waves do not repeatedly walk the same parent chains.
Expose the scheduler-depth benchmark through the Octane MCP benchmark tool.
37a8ca1: Index conditional JSX return value uses once per module so component-heavy TSRX modules no longer repeat a full AST scan for every component.
c84edbb: Propagate same-module fetch-tree warm reachability through reverse component edges instead of repeatedly rescanning every component. Deep TSrX component graphs now compile without a declaration-order-dependent fixed-point penalty while preserving opaque descendants, prop ownership, and synchronous cycles.
d5175ca: Keep virtual TypeScript generation working for computed object methods and create deferred FocusScope autofocus events in the scope element's DOM realm.
4a4996e: Treat
"use strong"as an author assertion that every user-authored render call is a pure projection of immutable snapshots and witnessed inputs. Condition local, dynamic, ordinary hook-shaped, callback-bearing, constructed, and tagged call shapes without React hook-name heuristics, while preserving compiler-proven hook setup, compatibility-mode live receivers, and changing event captures. Witness callable and receiver identities alongside explicit inputs, compare memoized component and ordinary-list projection inputs withObject.is, and preserve optional, aliased, cyclic, function-valued, or lexically shadowed setup-hook paths. Add bounded diagnostics for detectable state-snapshot mutations, cross-row writes from retained keyed scopes, and impure clock or random reads, and document the assumptions the production memoizer trusts.Expose the template-call memoization benchmark through the Octane MCP benchmark tool.
0.1.47
26 Aug 2026af0d999: Drain queued behavior-root interactions with amortized cursor compaction and constant-time pending-adoption bookkeeping so late modules and separately settling async adoptions stay linear while preserving FIFO and reentrant delivery. Expose the accompanying browser benchmark through the Octane MCP benchmark tool.
c800a1f: Allow nested TSRX
@{ ... }child blocks to contain setup statements, hooks, and no rendered JSX. Setup-bearing blocks now compile as scoped child render bodies in client, server, and hydration output, while render-only blocks remain transparent grouping.c1bb057: Keep compiler-generated local names compact in production modules with many components, reducing compile work and intermediate output size.
Patch Changes
af0d999: Drain queued behavior-root interactions with amortized cursor compaction and constant-time pending-adoption bookkeeping so late modules and separately settling async adoptions stay linear while preserving FIFO and reentrant delivery. Expose the accompanying browser benchmark through the Octane MCP benchmark tool.
c800a1f: Allow nested TSRX
@{ ... }child blocks to contain setup statements, hooks, and no rendered JSX. Setup-bearing blocks now compile as scoped child render bodies in client, server, and hydration output, while render-only blocks remain transparent grouping.c1bb057: Keep compiler-generated local names compact in production modules with many components, reducing compile work and intermediate output size.
97b9349: Skip unrelated sibling boundaries when pruning completed streaming SSR segments.
4393bea: Speed up production TSrX compilation for deep same-module component graphs by propagating automatic-memoization witnesses incrementally.
7dfef16: Speed up pure-host keyed-list upgrades and compiler queue walks while preserving adopted keyed nodes across suspended upgrade retries.
7e62361: Speed up development commits with many controlled form hosts by keeping diagnostic queue deduplication linear.
964783a: Keep development TSRX HTML-nesting diagnostics linear by deduplicating them with one identity set per compiled render plan instead of rescanning and serializing every diagnostic already collected for each new authored site.
d3dbd78: Skip sorting normalized client and server host props when no later raw alias changes their insertion order.
0.1.46
24 Aug 20267e96f71: Reduce streaming server-render work by checkpointing changed Suspense boundaries instead of copying the entire boundary registry for every component. Preserve render-phase retry state, discovery order, hydration seeds, and error handling.
Avoid general keyed-child bookkeeping for a single owned text node, and avoid reclassifying host subtrees that already require component reconciliation. Keep text identity, foreign DOM ownership, and interrupted-update rollback unchanged.
d7226ff: Add an experimental client-only Valdi writer compiler target with an explicit application-provided adapter contract, public compiler option types, and self-contained regression tests. Existing DOM and universal targets remain unchanged; no native runtime or application build integration is bundled.
0.1.45
24 Aug 20265b1e6a3: Fix missing root
onCaughtErrorreports for first-mount and parent-driven error boundary catches in non-suspending renders. Publish inline reports after the fallback's refs and layout effects commit, preserve the original error, and discard abandoned reports without duplicating existing scheduled-error reports.31abee5: Reduce generated client component code by sharing scalar-binding comparisons and renderable-child text updates through the private compiler runtime. Eligible repeated host rows retain inline comparisons to avoid extra calls and cache writes on unchanged bindings. Hydration avoids repeating attribute mutations when the server already has the final client value, and list-only reconciliation is separate from common text and function children.
Skip URL regular-expression checks only when the first character proves that the existing unsafe-protocol pattern cannot match. URL policy, controlled form values, authored evaluation order, mismatch recovery, and context propagation through unchanged child descriptors retain their existing behavior.
fd6ce69: Preserve canonical component wrappers across consecutive Vite hot updates so every save refreshes mounted DOM and universal-renderer components while retaining their own hook state. Keep default exports live and reload when an edit removes or invalidates a refresh boundary.
Patch Changes
5b1e6a3: Fix missing root
onCaughtErrorreports for first-mount and parent-driven error boundary catches in non-suspending renders. Publish inline reports after the fallback's refs and layout effects commit, preserve the original error, and discard abandoned reports without duplicating existing scheduled-error reports.31abee5: Reduce generated client component code by sharing scalar-binding comparisons and renderable-child text updates through the private compiler runtime. Eligible repeated host rows retain inline comparisons to avoid extra calls and cache writes on unchanged bindings. Hydration avoids repeating attribute mutations when the server already has the final client value, and list-only reconciliation is separate from common text and function children.
Skip URL regular-expression checks only when the first character proves that the existing unsafe-protocol pattern cannot match. URL policy, controlled form values, authored evaluation order, mismatch recovery, and context propagation through unchanged child descriptors retain their existing behavior.
fd6ce69: Preserve canonical component wrappers across consecutive Vite hot updates so every save refreshes mounted DOM and universal-renderer components while retaining their own hook state. Keep default exports live and reload when an edit removes or invalidates a refresh boundary.
5f7a457: Retain and retry client roots that suspend without a Suspense boundary. Keep initial roots empty and preserve committed UI, state, refs, and layout/passive effects during suspended updates, including structural replacements and portals. Retry the latest inputs, cancel abandoned work after supersession or unmount, and report actual resource rejections through normal error handling.
Retain server DOM while initial hydration is suspended, adopting the existing nodes, attaching refs, and running layout/passive effects only when hydration can commit.
Keep effect-thrown thenables on the error path and tear down roots on unhandled effect errors.
5227d7b: Retry incomplete descriptor and memoized subtrees before revealing Suspense content, preserving mounted state and DOM identity. Revisit discarded effect work after interrupted retries, keep descriptor text and props consistent during held transitions, and register deferred Activity effects when a cached hidden child descriptor becomes visible.
6927595: Fix strict browser TypeScript consumption of source-published chart bindings.
Recharts now publishes authored TypeScript for its chart utilities and state, resolves component imports explicitly, and exports the component implementations' own prop types. Visx supports strict browser source checks without Node globals. Remix Router's published declarations retain native anchor and form ref types. Redux Toolkit's query hooks type their bundler environment without Node globals.
Fix deferred native chart events, keep imperative and Cell refs off unrelated hosts, and resolve missing radial geometry without dropping data rows.
Octane accepts optional refs in composed ref arrays and supports nested ref arrays in
useImperativeHandle, including callback cleanup and primitive handles. Require the published TSRX compiler fix for ref-and-spread expressions rather than relying on a workspace-only patch.Publish the Volar compiler with its tested parser/printer dependencies and checked public declarations, preventing newer transitive printers from corrupting typed tuple parameters in installed consumers. Preserve generic Pie props and the native group targets of polar-axis events.
f1a7802: Match React's Suspense retry timing: share the 300 ms retry-commit budget across boundaries, keep sibling reveals atomic, and retain already-visible transition content indefinitely by default. Explicit finite transition fallback timeouts remain available.
Support promises thrown by resource readers on the client and server, and fix suspended-render cleanup, initial-state supersession, error reporting, and staged renderer ownership without delaying dependent data requests. Keep deferred hydration notifications and captured clicks behind the actual retry commit.
Let pending and error fallbacks suspend through an enclosing boundary without losing their state. Defer suspended error-fallback reports until reveal, cancel abandoned reports, and allow a server response to finish without waiting for an obsolete suspending fallback.
0.1.44
22 Aug 20269b06e47: Fix duplicated text when hydrating a sole primitive child that the server framed, including spread-bearing hosts and conditional children. Reuse the server Text node while preserving hydration mismatch suppression, native events, and later child updates.
7535acd: Deduplicate binding hook sub-slot derivation behind Octane's shared helper while preserving each binding's slotless and symbol-identity behavior.
0.1.43
21 Aug 20264b590bd: Improve Activity parity across compiled JSX, element descriptors, server rendering, hydration, and universal renderers. Hidden boundaries now disconnect public refs, preserve the latest authored styles and text, hide logically owned portals, and contain suspended work without activating an enclosing visible fallback. Retained insertion effects replay safely after suspended hidden renders, including memoized children and nested boundaries.
Support Activity aliases, namespaces, spreads, children props, and ordered keys without changing the direct mode-only compiler fast path. Integrate Activity visibility changes with ViewTransition enter/exit animations and expose native pseudo-element animations through the transition instance.
Coalesce hidden descendant visibility scans once per render wave and keep optional Activity implementation and ref tracking off unrelated application paths. Add production browser benchmarks and deterministic work, ref, and bundle controls. Octane's synchronous hidden-work scheduling and existing structural-transaction limitations remain unchanged.
c0ff085: Keep boolean renderable children empty when updated from text, and correctly reapply anchored child text after a held transition resumes. Explicit string conversion and typed text bindings retain their existing coercion semantics.
6a68a7d: Fold provider-proven immutable CSS-module class strings before template planning. Production Vite builds retain a live class reference in each static subtree so unused and lazy component styles keep their existing delivery boundaries. Mutable default maps remain dynamic unless their CSS provider supplies an explicit immutable-export contract.
Patch Changes
4b590bd: Improve Activity parity across compiled JSX, element descriptors, server rendering, hydration, and universal renderers. Hidden boundaries now disconnect public refs, preserve the latest authored styles and text, hide logically owned portals, and contain suspended work without activating an enclosing visible fallback. Retained insertion effects replay safely after suspended hidden renders, including memoized children and nested boundaries.
Support Activity aliases, namespaces, spreads, children props, and ordered keys without changing the direct mode-only compiler fast path. Integrate Activity visibility changes with ViewTransition enter/exit animations and expose native pseudo-element animations through the transition instance.
Coalesce hidden descendant visibility scans once per render wave and keep optional Activity implementation and ref tracking off unrelated application paths. Add production browser benchmarks and deterministic work, ref, and bundle controls. Octane's synchronous hidden-work scheduling and existing structural-transaction limitations remain unchanged.
c0ff085: Keep boolean renderable children empty when updated from text, and correctly reapply anchored child text after a held transition resumes. Explicit string conversion and typed text bindings retain their existing coercion semantics.
6a68a7d: Fold provider-proven immutable CSS-module class strings before template planning. Production Vite builds retain a live class reference in each static subtree so unused and lazy component styles keep their existing delivery boundaries. Mutable default maps remain dynamic unless their CSS provider supplies an explicit immutable-export contract.
6b97f85: Add opt-in CSS-module constant folding to one-shot Rspack and Rsbuild production builds. Authenticate immutable JavaScript CSS exports from the actual module graph, preserve stylesheet ownership, and keep proof callbacks on the main thread when compiler workers are enabled. Native CSS modules and mutable default maps retain their existing behavior.
0.1.42
20 Aug 20261581e1b: Skip already-drained scheduled flushes after synchronous native-event commits. Preserve commit-only effects, refs, Fragment bindings, transition finalization, profiling notifications, and microtask ordering.
afa3722: Preserve server-rendered descriptor components when a suspended Hydrate boundary resumes. Claim fallback cleanup ranges only after adoption completes, avoiding false hydration mismatch reports while preserving template-owned checks and removing genuinely unmatched content added during suspension.
231e248: Reduce keyed-row selection work for compiler-proven class-only updates. Preserve full row reconciliation for live renderable children and use the correct class setter for statically known HTML, SVG, and MathML templates. Also avoid unnecessary state-update allocations and focus traversal when a document has no focused control.
Patch Changes
1581e1b: Skip already-drained scheduled flushes after synchronous native-event commits. Preserve commit-only effects, refs, Fragment bindings, transition finalization, profiling notifications, and microtask ordering.
afa3722: Preserve server-rendered descriptor components when a suspended Hydrate boundary resumes. Claim fallback cleanup ranges only after adoption completes, avoiding false hydration mismatch reports while preserving template-owned checks and removing genuinely unmatched content added during suspension.
231e248: Reduce keyed-row selection work for compiler-proven class-only updates. Preserve full row reconciliation for live renderable children and use the correct class setter for statically known HTML, SVG, and MathML templates. Also avoid unnecessary state-update allocations and focus traversal when a document has no focused control.
2f9b301: Keep keyed row selection updates bounded when a row declares a constant alias for its key before rendering. Preserve the full affected-row bodies, event captures, strict-equality behavior, and transition replay.
939c64d: Keep
useMemoanduseCallbackon a universal renderer's hook runtime in production builds.The closure-free DOM memo optimization could incorrectly lower hooks inside an owning universal renderer component and import
octane/internal/client. Universal renderers do not have a DOM component scope, so those helpers either failed to resolve in custom build pipelines or crashed at runtime. Universal components now retain their renderer-specific memo hooks, while DOM components keep the optimized path.
0.1.41
19 Aug 2026489a886: Remove memo factory and dependency-array allocations from more production client cache hits, including nested expressions, returned JSX, custom hooks, plain TypeScript modules, and explicit hook slots. Preserve factory scope, declaration timing, callback identity, and held-transition rollback/promotion, and avoid the extra
useCallbackwrapper closure in every runtime.922b2d4: Avoid redundant external-store snapshot checks when an urgent DOM render is already queued. Keep universal-renderer subscriptions connected across snapshot and getter changes while preserving committed selectors, cleanup, and error handling. Avoid quadratic projection work for universal state-update queues that end in a replacement value.
814a3c1: Recognize unshadowed String conversions as template text and add an opt-in Node-only TypeScript project adapter for string-child inference. Keep conversion calls intact, reject stale source facts, omit uncertain type proofs, and share the same text classification across client compilation, SSR, and hydration. Publish declarations for the compiler and adapter APIs.
0.1.40
18 Aug 2026ff9b859: Parse authored TSRX modules through the native
oxc-tsrxcompatibility layer in Node to reduce compiler latency while preserving Octane's existing AST, source-map, stylesheet, and diagnostic contracts. Browser and other non-Node compiler consumers continue to use the pure-JavaScript@tsrx/coreparser.14b8b40: Update the bundled TSRX compiler to the latest installable release, including fixes for literal less-than text and tokenizer lookahead handling.
cc6e5ea: Extend Strong-mode analysis through statically known
useCallback,useEffectEvent, and memo-returned functions. Reject Effect Event calls during render and Effect Events in explicit hook dependency lists, while preserving supported hook usage and compatibility-mode behavior.
0.1.39
17 Aug 2026954028b: Stop the compiled
@forpath from retaining the entire descriptor renderer.mountItemdecided whether a de-opt list's items render through the plaindeoptItemBodyby comparing the body function by identity. The comparison is correct and cheap at runtime, butmountItemsits on the compiled@formount path that every list-rendering application reaches, so namingdeoptItemBodyfrom it is a live reference a bundler must honour. RetainingdeoptItemBodyretainschildSlot— the universal renderable-hole dispatcher — and through it the descriptor renderer,FragmentInstanceand fragment refs, portals, transitions, controlled-form restoration, focus preservation and the DOM attribute tables. Applications that only ever render compiled templates, and never present a generic renderable hole, shipped all of it.The fact is now recorded on the
ForSlotasplainDeopt, next to the existingmappedNativeflag, and stamped bychildSlotwhere the body is chosen inside the graph that already retains those modules.mountItemreads the flag instead of naming the function. The recorded value is exactly the identity test it replaces: within the de-opt branch the body isdeoptItemBodyprecisely when the compiler supplied no map body and this is not the mapped fallback, because both body-wrapping assignments are guarded by those same two conditions. BothForSlotliterals declare the field, so every slot keeps one hidden class and the stamp transitions nothing.Normalized production builds, gzip, via
benchmarks/bundle-size:application before after js-framework rows 40,752 20,609 TodoMVC 41,293 21,835 chat-stream 41,530 21,968 weather-app 49,524 31,742 The saving is entirely in the framework chunk (rows 38,380 → 18,237 B); every application chunk is unchanged, because no compiler output changed. The rows bundle drops from 559 surviving top-level declarations to 340 —
childSlot,FragmentInstance,deoptItemBody,renderPortalState,startTransitionandsetAttributeall become unreachable, whilereconcileKeyedandmountItemcorrectly remain. Theoctane-tsrxapplication, framework, and total budgets inbundle-size/app-budgets.json, and the reachability ceilings inbundle-size/minimal-budgets.json, are re-recorded against the new floor.This restores the reachability that held before the identity tests were introduced; the compiled output and every rendering path are unchanged.
21f4dfb: Compact safe keyed component rows in the Lynx main-thread first screen into shared host-template range commands.
The compiler now marks component-owned loops for the Lynx main renderer without granting it the broader background template-program capability. The first-screen renderer proves a single-root scalar/event host program, reuses its immutable shape, and sends one existing
mount-template-rangecommand per row while preserving every host ID, logical range, listener ID, first-tree snapshot, and background adoption identity. Unsupported, observable, nested, hidden, native-list, resource, and non-scalar shapes continue through the generic command path.In production fresh-page AB/BA runs this reduced 10,000-row public FCP from 1,626.1 ms to 1,411.9 ms (13.2%), exact all-row FCP from 1,596.5 ms to 1,408.4 ms (11.8%), and 30,000-row all-row FCP from 4,704.4 ms to 4,269.2 ms (9.3%). Against the merged main baseline, the controlled rows-zero bundle cost is 1,246 Web gzip bytes (0.92%) and 1,824 Lynx gzip bytes (1.12%).
1cb4a19: Give a component that calls a method-style custom hook its own update boundary.
A component that calls a hook owns that hook's state, so an update the hook schedules re-renders that component and nothing else. That held for
useThing()but not forobj.useThing()— and the object-carried shape is how a large part of the React ecosystem exposes hooks:route.useLoaderData(),api.useGetThingQuery(), and every hook returned by acreateXContext()factory.The
componentSlotLiteeligibility pre-pass decides whether a same-module component is hookless. Its body walk rejected a component on an unknown call only when the callee was anIdentifier, and the free-identifier sweep ahead of it only ever sees bare names, so neither test could observeapi.useCounter(): the receiver isapi, and the callee is aMemberExpression. A component whose only hook was member-form was therefore classified hookless and mounted throughcomponentSlotLite, aLiteBlockImplwith no block of its own. Its hook cells and theirforceUpdatebelonged to the parent, so updating that hook re-rendered the parent and every sibling under it, and the component could never bail out of a render it should have been isolated from.The slot-injection pass already handles these calls — it wraps them as
withSlot(sym, () => obj.useX(...args, sym))precisely because they are hooks so the two passes disagreed about the same syntax. The eligibility walk now applies the sameuse[A-Z]convention to the property name, and fails closed: matching only forfeits the lite path, never correctness.Components that genuinely have no hooks keep
componentSlotLiteexactly as before. Thebenchmarks/codegen-sizecorpus compiles byte-identically (raw 152564, min 75164, gz 26774 before and after), and compile time over 200 repetitions is unchanged within measurement noise (baseline 359.3-366.3 ms, candidate 362.2-372.0 ms across interleaved best-of-7 runs). Where the fix does apply, onecomponentSlotLitecall becomes acomponentSlotVoidcall — on the regression fixture, +28 bytes of emitted output for the component that gains a real block.
Patch Changes
954028b: Stop the compiled
@forpath from retaining the entire descriptor renderer.mountItemdecided whether a de-opt list's items render through the plaindeoptItemBodyby comparing the body function by identity. The comparison is correct and cheap at runtime, butmountItemsits on the compiled@formount path that every list-rendering application reaches, so namingdeoptItemBodyfrom it is a live reference a bundler must honour. RetainingdeoptItemBodyretainschildSlot— the universal renderable-hole dispatcher — and through it the descriptor renderer,FragmentInstanceand fragment refs, portals, transitions, controlled-form restoration, focus preservation and the DOM attribute tables. Applications that only ever render compiled templates, and never present a generic renderable hole, shipped all of it.The fact is now recorded on the
ForSlotasplainDeopt, next to the existingmappedNativeflag, and stamped bychildSlotwhere the body is chosen inside the graph that already retains those modules.mountItemreads the flag instead of naming the function. The recorded value is exactly the identity test it replaces: within the de-opt branch the body isdeoptItemBodyprecisely when the compiler supplied no map body and this is not the mapped fallback, because both body-wrapping assignments are guarded by those same two conditions. BothForSlotliterals declare the field, so every slot keeps one hidden class and the stamp transitions nothing.Normalized production builds, gzip, via
benchmarks/bundle-size:application before after js-framework rows 40,752 20,609 TodoMVC 41,293 21,835 chat-stream 41,530 21,968 weather-app 49,524 31,742 The saving is entirely in the framework chunk (rows 38,380 → 18,237 B); every application chunk is unchanged, because no compiler output changed. The rows bundle drops from 559 surviving top-level declarations to 340 —
childSlot,FragmentInstance,deoptItemBody,renderPortalState,startTransitionandsetAttributeall become unreachable, whilereconcileKeyedandmountItemcorrectly remain. Theoctane-tsrxapplication, framework, and total budgets inbundle-size/app-budgets.json, and the reachability ceilings inbundle-size/minimal-budgets.json, are re-recorded against the new floor.This restores the reachability that held before the identity tests were introduced; the compiled output and every rendering path are unchanged.
21f4dfb: Compact safe keyed component rows in the Lynx main-thread first screen into shared host-template range commands.
The compiler now marks component-owned loops for the Lynx main renderer without granting it the broader background template-program capability. The first-screen renderer proves a single-root scalar/event host program, reuses its immutable shape, and sends one existing
mount-template-rangecommand per row while preserving every host ID, logical range, listener ID, first-tree snapshot, and background adoption identity. Unsupported, observable, nested, hidden, native-list, resource, and non-scalar shapes continue through the generic command path.In production fresh-page AB/BA runs this reduced 10,000-row public FCP from 1,626.1 ms to 1,411.9 ms (13.2%), exact all-row FCP from 1,596.5 ms to 1,408.4 ms (11.8%), and 30,000-row all-row FCP from 4,704.4 ms to 4,269.2 ms (9.3%). Against the merged main baseline, the controlled rows-zero bundle cost is 1,246 Web gzip bytes (0.92%) and 1,824 Lynx gzip bytes (1.12%).
1cb4a19: Give a component that calls a method-style custom hook its own update boundary.
A component that calls a hook owns that hook's state, so an update the hook schedules re-renders that component and nothing else. That held for
useThing()but not forobj.useThing()— and the object-carried shape is how a large part of the React ecosystem exposes hooks:route.useLoaderData(),api.useGetThingQuery(), and every hook returned by acreateXContext()factory.The
componentSlotLiteeligibility pre-pass decides whether a same-module component is hookless. Its body walk rejected a component on an unknown call only when the callee was anIdentifier, and the free-identifier sweep ahead of it only ever sees bare names, so neither test could observeapi.useCounter(): the receiver isapi, and the callee is aMemberExpression. A component whose only hook was member-form was therefore classified hookless and mounted throughcomponentSlotLite, aLiteBlockImplwith no block of its own. Its hook cells and theirforceUpdatebelonged to the parent, so updating that hook re-rendered the parent and every sibling under it, and the component could never bail out of a render it should have been isolated from.The slot-injection pass already handles these calls — it wraps them as
withSlot(sym, () => obj.useX(...args, sym))precisely because they are hooks so the two passes disagreed about the same syntax. The eligibility walk now applies the sameuse[A-Z]convention to the property name, and fails closed: matching only forfeits the lite path, never correctness.Components that genuinely have no hooks keep
componentSlotLiteexactly as before. Thebenchmarks/codegen-sizecorpus compiles byte-identically (raw 152564, min 75164, gz 26774 before and after), and compile time over 200 repetitions is unchanged within measurement noise (baseline 359.3-366.3 ms, candidate 362.2-372.0 ms across interleaved best-of-7 runs). Where the fix does apply, onecomponentSlotLitecall becomes acomponentSlotVoidcall — on the regression fixture, +28 bytes of emitted output for the component that gains a real block.0fc84da: Add
@octanejs/tanstack-db: Octane live-query bindings for@tanstack/db. Re-exports@tanstack/db@0.7.0unchanged and ports the React live-query surface of@tanstack/react-db(useLiveQuery,useLiveInfiniteQuery,useLiveSuspenseQuery,useLiveQueryEffect,usePacedMutations) onto Octane hooks.useLiveQuery/useLiveSuspenseQueryrun on db's sharedcreateLiveQueryObserveranduseLiveInfiniteQueryon the coordinatedcreateLiveQueryWindowController, so status-only changes are observed, infinite-query windows are coordinated across hooks, and a failed page load rolls back and surfaces an error. Suspense integrates via Octane'suse(thenable).Allow browser-only TypeScript consumers to compile the reachable Octane client runtime without installing Node ambient types, while preserving the literal development-mode guards used for bundler substitution.
0.1.38
14 Aug 20260635af6: Complete React-compatible development diagnostics for unknown DOM properties, controlled form conflicts, invalid HTML nesting and hydration recovery, native and custom event listeners, and client/server resource hint arguments. Preserve Octane's native-event authoring model while keeping diagnostic helpers and full warning messages out of optimized production bundles.
0.1.37
13 Aug 2026954c75f: React Float conformance evidence, plus two small fixes it surfaced.
The 121 planned
ReactDOMFloat-test.jsparity-ledger cases are all dispositioned: 59 now carry executable adapted evidence (41 newly ported conformance tests across three suites plus links into the existing Float/hint suites), 54 are documented non-goals/divergences with per-case rationale (suspensey commits, whole-document containers, Fizz bootstrap/external-runtime protocol,<img>-preload scanning, SuspenseList, shadow-root scoping, streamed-boundary head content), and 8 stay planned against two newly-filed engine gaps, three warning-message families, and server-side hoistable prioritization ordering.Fixes: the document-metadata hoist partition is now HTML-scoped — nothing hoists from an SVG lexical context (a precedence link inside
<svg>no longer becomes a stylesheet resource;foreignObjectre-enters the HTML rules) — andpreinitModulewith an invalidasnow warns in development as documented instead of failing silently.94fa199: Tear down removed collapsed template runs with one
destroy-runcommand.Clearing a 10,000-row keyed table shipped a 100,000-command teardown stream (3.5 MB) from the background thread, acknowledged it with 70,000 per-host tombstone deltas (2.9 MB), expanded every removed collapsed row into seven logical records just to enumerate those commands, and left 70,000 generation tombstones per clear cycle on both threads. All four costs were O(hosts) for an operation whose input is O(runs).
The universal renderer now advertises a
teardownRunsdriver capability. When the driver accepts it, a removed subtree that is still a collapsed program-run instance with implicit contiguous ids skips expansion entirely and contributes to onedestroy-runcommand per contiguous range — the driver derives the event unbinds, removals, and post-order destroys from the program it already holds. Rows with refs, explicit ids, portals, or host callbacks keep the explicit per-host path, as does any driver that does not advertise the capability (the DOM boundary is unchanged).The Lynx binding negotiates the capability end to end (a new teardown-run readiness request base), expands the command against its dense record store to re-enter the certified teardown fast path — falling back to an accepted-records walk for reordered or explicit-path rows and to the general command loop for partial ranges — and acknowledges a full-run teardown with a single
remove-rundelta. Both threads then record one sorted retired-range tombstone instead of 70,000 map entries, and the background client retires compact metadata without materializing the handles it never observed.On the shared 10,000-row table, the clear command stream drops from 3,514,556 B to ~370 B, the clear acknowledgement from 2,929,786 B to ~813 B, and a create/clear/create cycle keeps the compact create acknowledgement from the previous change. Reused id ranges keep bumping generations, partial-range teardowns and rollbacks are pinned by new host-, client-, and emitter-side regression tests, and the existing certified-teardown and remount suites run unchanged.
c2e77a3: Keep plain function overload signatures non-ambient in the virtual TSX.
esrap before 2.3.2 printed
declareon every bodyless function declaration, so a plain overload pair next to its implementation typechecked as TS2384 ("Overload signatures must all be ambient or non-ambient") in the editor and undertsrx-tsc, on source that compiles and runs fine. The dependency floor now requires the fixed printer; an authored ambientdeclare functionkeeps its modifier.
Patch Changes
954c75f: React Float conformance evidence, plus two small fixes it surfaced.
The 121 planned
ReactDOMFloat-test.jsparity-ledger cases are all dispositioned: 59 now carry executable adapted evidence (41 newly ported conformance tests across three suites plus links into the existing Float/hint suites), 54 are documented non-goals/divergences with per-case rationale (suspensey commits, whole-document containers, Fizz bootstrap/external-runtime protocol,<img>-preload scanning, SuspenseList, shadow-root scoping, streamed-boundary head content), and 8 stay planned against two newly-filed engine gaps, three warning-message families, and server-side hoistable prioritization ordering.Fixes: the document-metadata hoist partition is now HTML-scoped — nothing hoists from an SVG lexical context (a precedence link inside
<svg>no longer becomes a stylesheet resource;foreignObjectre-enters the HTML rules) — andpreinitModulewith an invalidasnow warns in development as documented instead of failing silently.94fa199: Tear down removed collapsed template runs with one
destroy-runcommand.Clearing a 10,000-row keyed table shipped a 100,000-command teardown stream (3.5 MB) from the background thread, acknowledged it with 70,000 per-host tombstone deltas (2.9 MB), expanded every removed collapsed row into seven logical records just to enumerate those commands, and left 70,000 generation tombstones per clear cycle on both threads. All four costs were O(hosts) for an operation whose input is O(runs).
The universal renderer now advertises a
teardownRunsdriver capability. When the driver accepts it, a removed subtree that is still a collapsed program-run instance with implicit contiguous ids skips expansion entirely and contributes to onedestroy-runcommand per contiguous range — the driver derives the event unbinds, removals, and post-order destroys from the program it already holds. Rows with refs, explicit ids, portals, or host callbacks keep the explicit per-host path, as does any driver that does not advertise the capability (the DOM boundary is unchanged).The Lynx binding negotiates the capability end to end (a new teardown-run readiness request base), expands the command against its dense record store to re-enter the certified teardown fast path — falling back to an accepted-records walk for reordered or explicit-path rows and to the general command loop for partial ranges — and acknowledges a full-run teardown with a single
remove-rundelta. Both threads then record one sorted retired-range tombstone instead of 70,000 map entries, and the background client retires compact metadata without materializing the handles it never observed.On the shared 10,000-row table, the clear command stream drops from 3,514,556 B to ~370 B, the clear acknowledgement from 2,929,786 B to ~813 B, and a create/clear/create cycle keeps the compact create acknowledgement from the previous change. Reused id ranges keep bumping generations, partial-range teardowns and rollbacks are pinned by new host-, client-, and emitter-side regression tests, and the existing certified-teardown and remount suites run unchanged.
c2e77a3: Keep plain function overload signatures non-ambient in the virtual TSX.
esrap before 2.3.2 printed
declareon every bodyless function declaration, so a plain overload pair next to its implementation typechecked as TS2384 ("Overload signatures must all be ambient or non-ambient") in the editor and undertsrx-tsc, on source that compiles and runs fine. The dependency floor now requires the fixed printer; an authored ambientdeclare functionkeeps its modifier.125c861: Two server head-hoisting behaviors reach React Fizz parity, closing the last Float engine gaps from the React 19 parity audit.
Fallback hoistables are now suppressed transitively: a
<title>/<meta>/<link>authored inside a pending boundary's fallback never reaches the streamed head, including from a completed boundary nested inside that fallback — the fallback is discarded at reveal, but a streamed head line is permanent.Priority hoistables now lead the server head:
<meta charSet>serializes first (parsers only honor a charset within the first 1024 bytes), then<meta name="viewport">, then everything else in discovery order — matching React's ordering instead of pure discovery order.The parity ledger also gains evidence for the
identifierPrefixroot option and the external-store compatibility semantics (subscribe/snapshot/ server-snapshot throughuseSyncExternalStore), retiring those planned cases.765134a: Float precedence groups now form in tree discovery order on client mounts, and head hoists computed from setup locals no longer crash SSR.
Precedence group order is CSS cascade order. Client mounts used to create groups in the order component bodies finished executing — a nested child's or
@tryarm's group could precede its parent's, so the same tree could cascade differently on a client-only mount than on an SSR'd page. Resource registrations now run after a component's setup but before its children mount, so groups form parent-before-child, suspended arms at reveal, matching SSR and React on both sides.The server twin fixed a latent crash the same placement rule exposed: a hoisted head element or resource whose attribute reads a setup local (for example
<link href={slug} precedence …>afterconst slug = …) used to emit its registration ahead of the local's declaration and throw a TDZ ReferenceError during render. Capture-free registrations still lead the body — arm-root sheets keep shipping with the streaming shell — while ones that read setup locals now run right after setup, still ahead of children.9efd6f4: Add actionable development diagnostics for React-compatible stylesheet resource, ARIA attribute, CSS property, controlled-form, and event-listener authoring mistakes without retaining development warnings in optimized production builds.
603756a: Ship Float sheet resources discovered after the streaming shell.
Streaming SSR dropped
<link rel="stylesheet" href precedence>and<style href precedence>resources whose registration only ran on a post-shell pass — a sheet inside a nested pending boundary, or one whose href is computed from ause()resolution. Only the shell pass's head ever flushed, so streamed content revealed unstyled until hydration re-inserted the sheet client-side, and documents consumed without JavaScript never received the CSS at all.Each resolution wave now diffs the pass's per-resource sheet registrations against what is already on the wire and ships new tags with the wave chunk, ahead of its segment reveals: real markup in a hidden carrier (so no-JS consumers still get working CSS) plus an inline
$OCTRHcall that hoists the tags intodocument.headunder the client's precedence grouping. Once client Float resource state exists, the hoist hands each tag to the live runtime instead, keeping dedupe and group ordering in one authority, and a hydrating client adopts the streamed tags without duplicating. A still-pending child boundary's sheet rides its parent's reveal wave, matching React's hoisting of partial-boundary resources.
0.1.36
12 Aug 2026972fdd3: Reduce temporary allocations during keyed list reorders by safely reusing bounded numeric scratch buffers across reconciliations.
4a792e3: Allow universal renderers to route compiler-emitted thread-function helpers through an optional cold runtime module.
Lynx now uses that boundary to omit main-thread worklet registries and call bridges from applications that compile no worklets, while retaining late-chunk activation and the existing worklet-enabled behavior.
581b8bd: Reduce hydratable server-rendered HTML by reusing component-owned keyed-list ranges, self-delimiting pure-host descriptor items, and proven host-only conditional or switch ranges. Keep streamed Suspense segments parser-safe without expanding ordinary HTML tags or hydration comments.
Patch Changes
972fdd3: Reduce temporary allocations during keyed list reorders by safely reusing bounded numeric scratch buffers across reconciliations.
4a792e3: Allow universal renderers to route compiler-emitted thread-function helpers through an optional cold runtime module.
Lynx now uses that boundary to omit main-thread worklet registries and call bridges from applications that compile no worklets, while retaining late-chunk activation and the existing worklet-enabled behavior.
581b8bd: Reduce hydratable server-rendered HTML by reusing component-owned keyed-list ranges, self-delimiting pure-host descriptor items, and proven host-only conditional or switch ranges. Keep streamed Suspense segments parser-safe without expanding ordinary HTML tags or hydration comments.
24aa236: Skip unchanged keyed-list rows in production when their nested conditional content contains only host elements and every captured dependency is stable. Preserve conditional ownership, hydration, transitions, and existing keyed selection behavior while avoiding redundant DOM updates in TodoMVC-shaped apps.
9c397a2: Publish executable CommonJS conditions for Octane core, Floating UI, Base UI, and Radix while preserving their existing ESM and source-first entry points.
24aa236: Reduce server-rendered HTML escaping time and temporary allocations while preserving iterable snapshots, text security, streaming output, and hydration.
5377ef3: React Float behavioral parity: hoist exclusions and hint semantics.
Document-metadata hoisting now honors React's exclusions —
itemProp-bearing<meta>/<link>stay with theiritemScopehost, and metadata/resources that are direct children of<noscript>stay in the fallback content — on the client and the server, so hydration adopts the identical shape.Resource hints gain React's option semantics: font preloads always fetch anonymously (
crossorigin=""), preload-seeded connection/integrity options transfer onto the matchingpreinit's real tag (the server coalesces the redundant preload out of the head fold),preconnectidentity includes the CORS mode, responsive image preloads omit the fallbackhref, unknown option keys are dropped, and non-string hrefs warn in development and no-op. A module src is one executable identity acrosspreinitModuleand<script async type="module" src>in both the server pass and the hydrating client — no more duplicate module scripts in the cross pairings.6b65644: Minimize universal-renderer keyed placements with a longest-increasing-subsequence plan so distant swaps move only displaced hosts while preserving survivor identity, events, and lifecycle behavior.
f12a9a9: Type native HTML attribute spellings alongside React-compatible aliases, support native numeric attribute strings and SVG visibility attributes, and recognize native
readonlyspelling when checking text-input change handlers.972fdd3: Add compiler-visible descriptor children for bindings that inspect or clone ordinary TSЯX children.
1039b7d: Reduce duplicated client, server, and universal-renderer logic while sharing specialized compiled ref and server-spread helpers through renderer-isolated private runtime entry points. Preserve existing public helper exports, ref cleanup semantics, server rendering, hydration, and hot-path specializations.
ffadd39: React 19 parity: the implementation-gap tranche from the outstanding-work issue.
Nested document metadata:
<title>/<meta>/<link>and Float resources now hoist from ANY depth on the server too (React's model; the client always did) — a nested hoist serializes into the head channel at its authored position, the host body keeps only real children, and hydration adopts without mismatches. A hoist inside a conditional arm registers only while that arm renders.prerenderToNodeStream:octane/staticgains the stream variant resolves after the await-everything render completes;preludestreams the complete document bytes (scoped-style tags, then folded html). Nopostponedfield: postpone/resume stays a documented non-goal.Teardown errors reach the root callbacks: effect-cleanup and ref-detach throws during unmount report through
onCaughtError(boundary-claimed) oronUncaughtError(unclaimed) with routing semantics unchanged.Resource hints share the Float identity model:
preinit(as:'style')IS a stylesheet resource (honorsprecedence, joins the groups, dedupes against the rendered form),preinit(as:'script')dedupes against<script async src>,preload/preloadModuleafter the matching init no-op, image preloads withimageSrcSetkey on the srcset+sizes pair, and malformed calls warn in development.Universal renderer:
onCaughtError/onUncaughtErrornow apply tooctane/universalroots (boundary claims and scheduler-owned work; a directrender()throw remains the documented result channel, and there is noonRecoverableError— the universal renderer has no hydration channel).
a03ff0f: React 19 parity tranche: Float resources, root error callbacks, module resource hints, and partial-prerender/formState dispositions.
<link rel="stylesheet" href precedence>and<script async src>rendered at a component's body root are now React Float resources: hoisted intodocument.head, deduped by href/src across the page, stylesheet groups ordered by precedence (first-encounter group order, appended within a group), retained after unmount, emitted into buffered and streamed SSR head output, and hydration-deduped via the first client call's DOM seed. Suspend-until-loaded commits and<style href precedence>style resources remain documented non-goals —<style>in a component belongs to Octane's scoped-CSS system.preloadModuleandpreinitModulejoin the resource-hint set on both the client and server entries.createRoot/hydrateRootaccept React 19'sonCaughtError,onUncaughtError, andonRecoverableErroroptions (error-only signature — noerrorInfo/componentStack; defaults are unchanged when the options are absent).unstable_Activityis aliased toActivityfor React experimental-channel ports.React 19.2 partial pre-rendering (
resume/resumeAndPrerenderand the postpone/prelude protocol),cache()/cacheSignal(), andhydrateRoot'sformStateoption are recorded as documented non-goals in the parity ledger, anddocs/differences-from-react.mdnow documents the previously unlisted divergences:Context.Consumer,<title>child handling, per-compile-site metadata dedupe, hidden-<Activity>scheduling, dropped stream options,prerender's return shape, theuseIdformat, and theversionstring.4c1ecd1: React Float style resources, and the Context.Consumer decision made concrete.
<style href precedence>rendered at a component's body root is now a React Float STYLE RESOURCE: its plain CSS ships by href identity, sharing the stylesheet dedupe namespace and precedence-group ordering with link resources (data-precedence/data-hrefmarked, SSR-emitted, hydration-deduped, retained after unmount). Every other<style>keeps Octane's scoped-CSS behavior. Octane emits one tag per resource (no same-precedence merging), and CSS containing</stylefails closed in SSR with a development diagnostic.Context.Consumer stays modern-only, now with teeth: accessing
.Consumerin development logs a one-time migration diagnostic (and still returnsundefined, so feature probes match production), the upstream Consumer scenarios protecting observable behavior are ported asuseContext-reading conformance tests, and the render-prop-surface-only cases are recorded as parity-ledger non-goals.
0.1.35
11 Aug 202650b7988: Cache proven immutable state-array filter projections and skip redundant single-token class updates while preserving controlled field restoration.
6daa380: Start independent asynchronous children in proven bounded keyed lists during the same discovery wave while preserving existing suspense and request reuse.
d2c9e1c: Reuse delegated-event accessors and capture paths, and omit redundant component markers for proven exhaustive switch-root components.
Patch Changes
50b7988: Cache proven immutable state-array filter projections and skip redundant single-token class updates while preserving controlled field restoration.
6daa380: Start independent asynchronous children in proven bounded keyed lists during the same discovery wave while preserving existing suspense and request reuse.
d2c9e1c: Reuse delegated-event accessors and capture paths, and omit redundant component markers for proven exhaustive switch-root components.
01240e6: Speed up universal and Lynx rendering with shared compiled host-template programs, range-based host and listener identities, compiler-proven ownership elision, capability-negotiated compact transport acknowledgements, lazy public handles and query selectors, and lower-allocation reconciliation. Preserve native events, late refs, worklets, lists, portals, first-screen adoption, public handles, and rendered output.
59a35ae: Batch component-owned universal host templates after Lynx first-screen adoption, preserving component hooks, effects, keyed identity, native events, and safe cross-thread public handles while reducing large-list creation overhead.
a8b432b: Retain unchanged keyed component subtrees for asynchronous universal renderers, avoiding redundant Lynx row renders while preserving context updates, native listeners, host identity, effect lifetimes, and accepted-commit ordering.
910c240: Specialize proven mixed inline styles so static declarations enter the template and only changed dynamic properties update the DOM.
db5687e: Start provably reachable lazy component imports alongside independent suspended work without eagerly loading dormant deferred-hydration islands.
e2466a5: Reuse already-warmed asynchronous resources across promoted transition retries without duplicating request creators or exposing partially committed screens.
2d06817: Preserve active IME composition and focus through controlled-input updates, keyed reorders, and deferred hydration; improve Samsung Internet and Android browser compatibility, scheduling, and browser build targets.
0.1.34
10 Aug 202678316b4: Publish executable CommonJS conditions for Octane core, Floating UI, Base UI, and Radix while preserving their existing ESM and source-first entry points. Source-package discovery still recognizes those packages when the CommonJS build has not been generated yet.
4e53ef4: Complete React Fragment ref parity across JSX aliases and spreads, direct element descriptors, server rendering and hydration, strongly typed refs, text and portal ownership, events, observers, focus, geometry, document positioning, and scrolling.
4cc7840: Reuse server-provided hydration data before creating compiler-owned async requests, preventing duplicate client fetches while preserving Suspense and external hydration ownership.
Patch Changes
78316b4: Publish executable CommonJS conditions for Octane core, Floating UI, Base UI, and Radix while preserving their existing ESM and source-first entry points. Source-package discovery still recognizes those packages when the CommonJS build has not been generated yet.
4e53ef4: Complete React Fragment ref parity across JSX aliases and spreads, direct element descriptors, server rendering and hydration, strongly typed refs, text and portal ownership, events, observers, focus, geometry, document positioning, and scrolling.
4cc7840: Reuse server-provided hydration data before creating compiler-owned async requests, preventing duplicate client fetches while preserving Suspense and external hydration ownership.
39b3e19: Reuse proven stable returned-JSX component regions inside context providers while preserving context updates, component state, and server-rendered DOM adoption.
8c29020: Reuse unchanged derived JSX descriptor arrays inside context providers, avoiding unnecessary keyed reconciliation and memo comparisons while preserving context updates, component state, and hydration.
97e65b9: Skip compiler-proven stable hookful child component call sites during unchanged parent updates while preserving independently scheduled child state, context, refs, effects, and hydration.
0.1.33
10 Aug 20261fe297e: Match React's autofocus behavior during server rendering, client mounting, and hydration, and restore focus and text selection after DOM updates.
db0d495: Add behavior-only roots for server-rendered and externally streamed DOM, with explicit range ownership, readiness, trusted native event adoption, cancellation, and DOM-preserving disposal.
677182d: Avoid reconciling unchanged compiler-cached renderable children while preserving context and hidden-tree effect lifecycles, cache safe derived values in hook-using JSX components, and omit fetch-warming scaffolding from component trees proven to contain no asynchronous work.
Patch Changes
1fe297e: Match React's autofocus behavior during server rendering, client mounting, and hydration, and restore focus and text selection after DOM updates.
db0d495: Add behavior-only roots for server-rendered and externally streamed DOM, with explicit range ownership, readiness, trusted native event adoption, cancellation, and DOM-preserving disposal.
677182d: Avoid reconciling unchanged compiler-cached renderable children while preserving context and hidden-tree effect lifecycles, cache safe derived values in hook-using JSX components, and omit fetch-warming scaffolding from component trees proven to contain no asynchronous work.
3fb96df: Fix a sibling-ordering bug in hosts whose children are all components: a hookless child that rendered nothing at mount (for example a sole
@ifwith no@else) inserted content produced by a later render after its later siblings instead of at its own source position. The compiler now keeps per-child anchors for such hosts, while hosts whose children provably hold their position keep the marker-elided form.677182d: Reduce server-runtime initialization retention and production compiler output while preserving hydration, streaming, component-owned events and styles, View Transitions, and callback identity.
4653a2e: Fix a dev-only crash ("Cannot read properties of undefined (reading 'block')") when a component is invoked as a plain function — for example
Row({ label })inside another component's render or a.mapcallback. The HMR wrapper now stays transparent to scope-less direct calls, matching production behavior, and an edit still refreshes the call site's output through the caller's hot update.7282555: Preserve React-compatible inline placement and event propagation for resource links with explicit load or error handlers. Keep dynamically hoisted metadata listeners synchronized across capture and bubble updates, hydration, and unmount.
3d09348: The compiler now lowers React-style conditional JSX returns (
if (c) return <A/>; return <B/>;, including ternary returns) to the same template control flow as@if/@elsewhen the branch shapes are provably remount-equivalent under React semantics, so branch-selected output stops running through the de-opt descriptor renderer on both client and server. Hooks, direct-call helpers, fragment arms, same-type arms, self-recursive arms, and every other return shape keep the established value ABI.8cb40df: The compiler's single-root proof is now transitive: a component whose
@{}body is an@if/@elsetree where every arm renders exactly one plain host element or one qualifying same-module component call is proven single-root through a fixed point, so its call sites (including multiple component children of one host) mount with the existing anchorless self-marked regime instead of minting a<!--comp-->/<!--/comp-->pair each. Client-mount elision only — SSR output and hydration adoption are unchanged. On the spa-navigation benchmark's 1024-leaf route this removes all 4,092 per-slot marker comments and their insertions.677182d: Preserve compiler-hook registration and TanStack Start client hydration when their bootstrap entrypoints are imported for side effects in production bundles, while keeping unrelated package modules tree-shakeable.
fc1c146: Tree-shake unused Three.js constructors from compiled scenes and keep direct Three renderer roots independent of the DOM runtime while preserving full Canvas catalogues, context providers, and mixed-renderer scheduling.
a84fcaa: Avoid unnecessary asynchronous warming for synchronous components that read plain props, preventing speculative getter evaluation and reducing generated render work.
217a0b5: Unmount teardown now removes a deleted subtree's DOM once at the outermost detached block instead of per-descendant range (portals still self-detach from their foreign targets), and the de-opt ref-detach walk is skipped for subtrees that never stamped a descriptor ref. A full-page teardown drops from thousands of
removeChildcalls to one per top-level node, and deletion cleanups now observe the entire deleted subtree still attached — matching React's commitDeletionEffects order.
0.1.32
9 Aug 2026d453832: Reduce server-rendering overhead and managed document metadata while preserving streaming, hydration, and router asset identity.
3152f0b: Reduce universal keyed-scene reconciliation and Three renderer lifecycle and frame-subscriber overhead while preserving object identity, transactional cleanup, and priority ordering.
1c44117: Reduce production application and package-version bundle sizes while preserving profiling, transition, and hydration behavior.
Patch Changes
d453832: Reduce server-rendering overhead and managed document metadata while preserving streaming, hydration, and router asset identity.
3152f0b: Reduce universal keyed-scene reconciliation and Three renderer lifecycle and frame-subscriber overhead while preserving object identity, transactional cleanup, and priority ordering.
1c44117: Reduce production application and package-version bundle sizes while preserving profiling, transition, and hydration behavior.
cbd55ca: Make transported commit payloads proportional to change size, not tree size.
On a transported root (Lynx's dual-thread renderer, any process split), every object-valued host prop is re-encoded through the wire clone on every render, so the identity-based prop diff could never certify sameness: a single-row select over a 10,000-row table shipped a ~2.4MB commit of ~30,000 commands one spurious
updatefor every row's freshly rebuilt class array plus twoeventre-binds per row — identical in size to a commit where every tenth row actually changed. Three cuts fix it:The prop diff now uses
sameUniversalHostPropValue, an exported, depth-limited structural equality over the value semantics the wire itself preserves: primitives byObject.is, arrays and plain records (from any realm, via the universal-sidehasCrossRealmPlainPrototype) element-by- element to depth 2, everything deeper or exotic by identity. Equal values emit no command, andcloneSerializableValueaccepts cross-realm plain objects through the same predicate.The universal compiler lowers
class={[…]}arrays whose elements are statically string-or-falsy to their clsx-composed string expression (an all-literal array folds to a static plan prop), and folds string-literal expression children and literal attribute values into the frozen plan, so the hottest per-row slot values are primitives instead of fresh allocations.A re-created but equivalent event handler closure no longer re-announces its listener on the wire: the listener ID is stable and the background dispatch table always rebinds to the newest closure, so only a new listener, a priority change, or an owner change emits a command. Host callbacks (attach) keep closure-identity announcement — their re-run on handler replacement is an observable contract.
The same select commit now carries 2 commands / ~225 bytes at any table size, and update-every-10th carries exactly one text update per changed row (the
lynx-tableratio guards tighten from 1500× the changed-rows floor to 1.0×). The "renders scheduled while a commit awaits acknowledgement coalesce into one commit carrying the latest state, intermediate states never cross" transport behavior is now a tested contract — it previously held only as a side effect of acknowledgement timing, and becomes load-bearing for storm throughput once commits stop saturating the main thread.cdb501c: Preserve proven single-root memo component shapes and keep catch-only error boundaries independent of optional Suspense and transition startup capabilities.
0.1.31
8 Aug 202680a9c7e: Compiler-inferred component memoization now admits destructured props parameters.
function Child({ rows })is the same one-props snapshot asfunction Child(props), so production call sites of such components gain the whole-region dependency cache and skip the child entirely when their props are reference-stable — patterns that evaluate expressions of their own (defaults, computed keys), bindcurrent, or use array destructuring still fall back. Call-site eligibility is also no longer vetoed body-wide by an unrelatedref.currentor live-import member read elsewhere in the parent: the reads that actually flow into a site (directly or laundered through a local) still reject that site, everything else keeps its region.62d7f13: Compiler-inferred component memoization now admits spread bags on host elements.
<path d={e.d} {...e.attrs}>inside a component body no longer disqualifies that component's region cache or its keyed-list caches: a host spread is one runtime-diffed binding whose bag is reachable only from dependencies the region guard already witnesses, so skipping on unchanged dependencies is exactly the no-op a re-entry would have been. Re-entries keep full spread semantics — changed keys apply, vanished keys clear, and spread-supplied refs and event handlers attach, swap, and detach as before. Spreads on component tags keep failing closed: they build the child's props snapshot, which cached call sites must never construct from a getter-bearing bag.16df26e: Skip unchanged keyed rows containing nested host-only lists or conditionals while preserving imported dependency invalidation and structured row ownership.
0.1.30
8 Aug 202610011bb: Reduce temporary allocations during keyed list reorders by safely reusing bounded numeric scratch buffers across reconciliations.
081fa1e: Avoid reconciling unchanged compiler-cached renderable children while preserving context and hidden-tree effect lifecycles, cache safe derived values in hook-using JSX components, and omit fetch-warming scaffolding from component trees proven to contain no asynchronous work.
60004f0: Reduce component-render bookkeeping when mounting compiler-certified keyed host rows while preserving ordinary DOM insertion, hydration, row identity, delegated events, lifecycle behavior, and existing public APIs.
Patch Changes
10011bb: Reduce temporary allocations during keyed list reorders by safely reusing bounded numeric scratch buffers across reconciliations.
081fa1e: Avoid reconciling unchanged compiler-cached renderable children while preserving context and hidden-tree effect lifecycles, cache safe derived values in hook-using JSX components, and omit fetch-warming scaffolding from component trees proven to contain no asynchronous work.
60004f0: Reduce component-render bookkeeping when mounting compiler-certified keyed host rows while preserving ordinary DOM insertion, hydration, row identity, delegated events, lifecycle behavior, and existing public APIs.
27758f5: Skip unchanged keyed-list rows in production when their nested conditional content contains only host elements and every captured dependency is stable. Preserve conditional ownership, hydration, transitions, and existing keyed selection behavior while avoiding redundant DOM updates in TodoMVC-shaped apps.
136b0e3: Reduce server-runtime initialization retention and production compiler output while preserving hydration, streaming, component-owned events and styles, View Transitions, and callback identity.
d69ab86: Reduce server-rendered HTML escaping time and temporary allocations while preserving iterable snapshots, text security, streaming output, and hydration.
1a27e19: Update only the previously selected and newly selected keyed-list rows when the production compiler can prove that a row's selection depends solely on its own key. Preserve component rerenders, immutable updates, and existing lifecycle behavior without requiring a new public API.
7f6a134: Preserve compiler-hook registration and TanStack Start client hydration when their bootstrap entrypoints are imported for side effects in production bundles, while keeping unrelated package modules tree-shakeable.
ce68bb8: Stop rebuilding a component's children when an unrelated context updates.
A component's children resolve lazily, in the scope they render in, so the compiler wraps each one in a thunk that builds its element and its props object on demand. That thunk was rebuilt whenever a single global context epoch moved, and the epoch moved on every provider value change anywhere in the tree. A rebuild re-runs the thunk, so the props object came back new, and with it every inline callback and object literal written at that call site, even though the component that authored them never rendered again.
Anything keyed on those identities churned:
useEffect,useCallback, anduseMemodependency arrays compared unequal every time, andmemo()on such a child could not bail out. Where one of those effects wrote state that fed the same provider, the cycle never converged.@octanejs/radixFormhit exactly that: aForm.Messagewith a functionmatchre-registered and unregistered its matcher forever, one round per frame, for as long as the form stayed mounted.The rebuild is now keyed on the contexts a record actually read while resolving. Reads are collected during resolution and re-checked against their context's version, so a record that reads no context is never rebuilt by a provider update and keeps the props it was given. The resolving scope is still honoured for records that do read context, which is what keeps one shared descriptor from serving two providers, and a lazily read context value still refreshes on the render after it changes.
Scope alone no longer forces a rebuild either. Host classification resolves a child in the parent block before the child block renders it, so the two scopes alternate every render; combined with the check above that alternation was re-creating props on its own.
fbe0d39: Enforce Strong-mode render purity inside lazy state initializers, linked-state reconcilers, and their source/value equality callbacks without restricting deferred work or compatibility-mode applications.
9fa0b47: Reduce production JSX rendering overhead for compiler-proven nested components while preserving authored component returns, hooks, context, hydration, and existing public APIs.
0.1.28
6 Aug 20262b98a33: Universal target: the root component now carries a committed range, so a state update in the root component itself replays through the scoped path instead of falling back to a whole-root attempt (issue #574 follow-up). Replays also adopt committed component subtrees whose props, state, contexts, and code revision are provably unchanged instead of re-rendering and re-drafting them, and child owner claims resolve through a positional fast path when the render keeps its committed order. In the issue's repro shape with state at the root, one press beside 4,000 untouched siblings drops from ~55 ms to ~10 ms, and clean-subtree adoption also flattens the cost of large in-scope replays such as list components re-rendering unchanged items.
0.1.27
6 Aug 202646e1833: Keep freshly mounted Suspense content hidden when hydration resumes inside a hidden Activity.
5a8e807: Keep hidden Activity DOM hidden when a descendant independently replaces its output.
State updates, error and Suspense retries, and accepted hot-module updates now reapply the nearest hidden Activity's visibility after rendering. Replacement elements and text remain hidden until the Activity reveals, while authored display and text values are restored correctly on reveal. Activity and Suspense now share hide ownership for overlapping DOM, so either boundary can reveal first without capturing the other's temporary hidden styles.
0.1.26
5 Aug 20261f01b08: Refresh compiled component output correctly after accepted hot updates.
Hot refresh now discards the outgoing compiler-owned template and slot layout before mounting the new body, while retaining the component block and its hook state. Exclusively owned markerless output is promoted to a durable component range during refresh, so static markup edits and newly added component calls update immediately in both mounted and hydrated applications instead of leaving stale DOM or throwing during insertion. When an enclosing control-flow branch shares that root as its boundary, the update safely falls back to a page reload instead.
48e2397: Keep universal state updates proportional to their retained owner subtree: a leaf
setStatereplays only its owning component, keyed-list item state and several owners updated by one event replay their nearest shared component ancestor instead of the root, updates under an idle@try/Suspense boundary stay scoped (active episodes and retained-hidden content still replay from the root, and a scoped render error falls back so the boundary catches it), structural updates that insert, reorder, or remove hosts commit through the scope's physical frame, compact leaf rows driven by list state update within their owning list component, and scoped commits edit the accepted listener tables in place instead of cloning them. Also avoid cloning the object driver's full instance map when preparing a small host batch, and expose the corresponding benchmark through the MCP server.
0.1.25
4 Aug 2026bd8bb1b: Require Node.js 22.22.2 or newer across Octane's published packages.
Add the
octane/compiler/registerpreload for running server and SSG scripts directly with Node or Bun. It compiles imported.tsrx/.tsxmodules and plain TypeScript custom hooks in server mode without a Vite build. Bun also targets bareoctaneimports atoctane/serverin pass-through authored source dependencies, including packages that manage their hook slots manually.
0.1.24
4 Aug 2026ec77602: Fix Rspack and Rsbuild development builds crashing while evaluating hot
.tsrxmodules by aliasing webpack HMR metadata before reading dispose data.29c5bdb: Track the receiver of one-level method calls in inferred hook dependencies (#542).
useMemo(() => count.toFixed(2))used to infer[count.toFixed]— a function that lives onNumber.prototypeand therefore never changes identity, so the memo stayed frozen at its first value whilecountmoved. The same hazard applied to any prototype method called on a one-level receiver, including instance methods of replaced class instances.Neither static alternative is right for every program: depending on
countfixes primitives but would re-runprops.onChange(...)hooks on every parent render, becausepropsis a fresh container whose own function property is the real dependency. The inferred array now compiles such calls to__methodDep(root, 'name'), a new semi-public runtime helper that picks the comparable value per render: the member when it is an own property of the receiver, the receiver when the method is inherited, andundefinedwhen the property is absent (soprops.onReady?.()stays inert until a handler is passed). Own-property callbacks and absent optional handlers keep exactly their previous recompute behavior; inherited-method calls now correctly recompute when the receiver changes.Deeper callees (
a.b.c(...)) and computed callees (a[k](...)) already tracked their receivers and are unchanged, as are explicit dependency arrays.9b032d8: Treat closures marked with an other-context directive as opaque to dependency inference (#542, problem 2).
A nested function whose directive prologue declares that its body executes outside render —
'use gpu'(TypeGPU shader code) or'worklet'(Reanimated/worklets-core UI-thread code) — now contributes only its root captures to an inferred dependency array. Previously the array hoisted the closure's member reads to render time, which evaluated context-bound getters such as TypeGPU's.$where they are illegal, forcing an explicit dependency array. The TypeGPU example from the issue now infers[root, timeUniform, hueUniform]with no array written.The directive list is a deliberate allowlist and can grow. Same-context hints (
'use strict', React Compiler's'use memo'/'use no memo') and directives reserved for other semantics ('use server','use client','use cache','use workflow') never truncate; closures without a listed directive keep today's member-path inference.
Patch Changes
ec77602: Fix Rspack and Rsbuild development builds crashing while evaluating hot
.tsrxmodules by aliasing webpack HMR metadata before reading dispose data.29c5bdb: Track the receiver of one-level method calls in inferred hook dependencies (#542).
useMemo(() => count.toFixed(2))used to infer[count.toFixed]— a function that lives onNumber.prototypeand therefore never changes identity, so the memo stayed frozen at its first value whilecountmoved. The same hazard applied to any prototype method called on a one-level receiver, including instance methods of replaced class instances.Neither static alternative is right for every program: depending on
countfixes primitives but would re-runprops.onChange(...)hooks on every parent render, becausepropsis a fresh container whose own function property is the real dependency. The inferred array now compiles such calls to__methodDep(root, 'name'), a new semi-public runtime helper that picks the comparable value per render: the member when it is an own property of the receiver, the receiver when the method is inherited, andundefinedwhen the property is absent (soprops.onReady?.()stays inert until a handler is passed). Own-property callbacks and absent optional handlers keep exactly their previous recompute behavior; inherited-method calls now correctly recompute when the receiver changes.Deeper callees (
a.b.c(...)) and computed callees (a[k](...)) already tracked their receivers and are unchanged, as are explicit dependency arrays.9b032d8: Treat closures marked with an other-context directive as opaque to dependency inference (#542, problem 2).
A nested function whose directive prologue declares that its body executes outside render —
'use gpu'(TypeGPU shader code) or'worklet'(Reanimated/worklets-core UI-thread code) — now contributes only its root captures to an inferred dependency array. Previously the array hoisted the closure's member reads to render time, which evaluated context-bound getters such as TypeGPU's.$where they are illegal, forcing an explicit dependency array. The TypeGPU example from the issue now infers[root, timeUniform, hueUniform]with no array written.The directive list is a deliberate allowlist and can grow. Same-context hints (
'use strict', React Compiler's'use memo'/'use no memo') and directives reserved for other semantics ('use server','use client','use cache','use workflow') never truncate; closures without a listed directive keep today's member-path inference.f9b2731: Observe promises recreated by plain async components during server replay, so rejected components render their
@catcharm without emitting duplicate unhandled rejections.6714914: Keep host refs unpublished while an initial Suspense primary is hidden, and avoid detaching replacement refs that never committed before a suspended update.
0.1.23
2 Aug 2026c1ad31b: Compile a
@{ … }body that returns without reaching an output node.A shorthand block whose statements return but which never reaches a trailing JSX node crashed the client compiler with
Cannot read properties of undefined (reading 'type'):export function CoreGameFunction(props) @{ const [boardData, setBoardData] = useState([null, null, null]); return null; }@{ … }desugars to a trailing return, so a body carrying any value return is compiled as one whose output is a returned value rather than an emitted template. That classification is driven by the returns alone, but lowering the tail assumed the block also had a render node to lower, and the parser leaves that node null when the block ends on a statement. The crash was not specific touseStateor tonull: a block holding onlyreturn <div />failed the same way, as did one holding onlyreturn 1. It was client-only — the server compiler already treated the same shape as having an empty tail.A block with no output node now compiles as what it is: the body's own returns are the whole output, and falling off the end renders nothing. Setup still runs, so slot-keyed hooks keep their state across whichever value the body returned. This is the shape a component is in while it is being written, and the shape a React-style
return <jsx>inside a block already had.The synthesized tail is dropped where it cannot be reached — when the block ends in a
returnor athrow, or in anif/elsewhose arms both do. Reachability is proven syntactically and nothing subtler is attempted, because keeping the tail is always correct: a body that falls through toundefinedis how a compiled body reports that it already emitted its template.
0.1.22
31 Jul 202643df1f9: Evaluate an inline event handler's arguments when the event fires, not on every render.
onClick={() => setData(makeData(1000))}compiled to a{ fn, args }bundle whose argument was lifted into the component body, somakeData(1000)ran on mount and on every subsequent render even if the button was never clicked. An argument is now bundled only when evaluating it early is observationally equivalent to evaluating it on the event: identifiers, plain member paths, literals, and operators over those. Calls, fresh array/object/regex literals, mutations,ref.current(the ref attaches after the mount that would read it, so the first event received the pre-attachnull), and computed member keys keep the ordinary closure handler.Inline handlers that read nothing which can change between renders are now installed once at mount instead of being rebuilt and reassigned on every render, matching what a named handler already received. Handlers inside a keyed
@forrow are excluded, since a surviving row can be handed a different item without remounting.7a112b4: Reuse scoped JSX value records when host classification hands them directly to the host's child block, avoiding duplicate descriptor construction while preserving context-scoped resolution.
0.1.21
30 Jul 202610efc28: Fix hydration mismatch recovery corrupting the DOM when a branch adopts a non-spanning server range.
When an
@if/@switch(or an@if-lowered ternary) hydrates a slot whose server content leads with a nested<!--[-->…<!--]-->pair, the branch adopted that first pair as its own range without checking that the pair spans the whole slot. Against a differently-encoded server shape — for example a legacy value-hole list serialized as an outer pair plus one pair per keyed item — the branch then owned only a prefix of the slot: the next branch swap left the stranded remainder on screen next to the new arm, and re-entering the original arm mounted into detached anchors, rendering nothing (or throwingNotFoundErroron insertion, depending on the shape). Recovery must never crash or leak — it is the production safety net for stale server HTML.The branch adoption now verifies the nested pair reaches the slot's close marker. When it does not, the runtime treats it as a structural hydration mismatch: it warns in development, discards the server range, and client-builds the branch fresh with hydration suspended for that subtree, so cursor-greedy adoption (such as a keyed list's markerless item path) cannot claim the slot's own close marker.
39bfc49: Fix a crash when a sole-child value hole leaves and re-enters array mode.
An element whose only child is a
{expr}hole renders markerless (the slot owns the whole element), and an array value lazily mints a comment marker pair inside the element to anchor the keyed list. Clearing the slot on a later kind flip swept those markers out of the DOM but kept the slot's references to them, so the next mount anchored on detached comments: flipping array → text/null/host → array crashed withCannot read properties of null (reading 'nodeType'), and array → component threwNotFoundErrorwhile inserting the new content. The owns-parent clear now forgets the swept marker pair, so re-entering array mode re-mints a live pair and every other regime returns to the markerless baseline.4863b39: Render the non-JSX arm of a ternary child hole instead of discarding it, and stop crashing on a
nullconsequent.{cond ? A : B}at a JSX child position lowers to an ifBlock when either arm is a JSX literal. The other arm's value — a keyed.map(…)array, a string, a variable holding an element, a nested ternary, a falsy primitive — was compiled into the branch helper as a bare expression statement, so its value was evaluated and dropped and the hole rendered empty. React renders that arm's value, and server rendering already did too, so the same component could ship content from the server that the hydrating client blanked out. Anullconsequent ({cond ? null : <Jsx/>}) did not compile at all: the lowering crashed reading the missing branch.A non-JSX arm now lowers to the authored-equivalent
<>{expr}</>, so the branch renders the value through the fragment's child hole: keyed.maparms take the same keyed fast path as@for, nested ternaries become nested ifBlocks, and primitives (including0) render as text. Anull/falseconsequent compiles to an ifBlock with an empty then branch, mirroring the long-supportednullalternate. The server now claims template-form ternary holes symmetrically (ssrControl+ arm ranges, exactly like an authored@if), so the hydrating client adopts the taken arm's DOM — including keyed list arms — instead of relying on shape coincidence. Value-form positions (returned.tsxtrees, whose holes the client folds into descriptor value holes) keep their existingssrChildoutput byte-for-byte.
Patch Changes
10efc28: Fix hydration mismatch recovery corrupting the DOM when a branch adopts a non-spanning server range.
When an
@if/@switch(or an@if-lowered ternary) hydrates a slot whose server content leads with a nested<!--[-->…<!--]-->pair, the branch adopted that first pair as its own range without checking that the pair spans the whole slot. Against a differently-encoded server shape — for example a legacy value-hole list serialized as an outer pair plus one pair per keyed item — the branch then owned only a prefix of the slot: the next branch swap left the stranded remainder on screen next to the new arm, and re-entering the original arm mounted into detached anchors, rendering nothing (or throwingNotFoundErroron insertion, depending on the shape). Recovery must never crash or leak — it is the production safety net for stale server HTML.The branch adoption now verifies the nested pair reaches the slot's close marker. When it does not, the runtime treats it as a structural hydration mismatch: it warns in development, discards the server range, and client-builds the branch fresh with hydration suspended for that subtree, so cursor-greedy adoption (such as a keyed list's markerless item path) cannot claim the slot's own close marker.
39bfc49: Fix a crash when a sole-child value hole leaves and re-enters array mode.
An element whose only child is a
{expr}hole renders markerless (the slot owns the whole element), and an array value lazily mints a comment marker pair inside the element to anchor the keyed list. Clearing the slot on a later kind flip swept those markers out of the DOM but kept the slot's references to them, so the next mount anchored on detached comments: flipping array → text/null/host → array crashed withCannot read properties of null (reading 'nodeType'), and array → component threwNotFoundErrorwhile inserting the new content. The owns-parent clear now forgets the swept marker pair, so re-entering array mode re-mints a live pair and every other regime returns to the markerless baseline.4863b39: Render the non-JSX arm of a ternary child hole instead of discarding it, and stop crashing on a
nullconsequent.{cond ? A : B}at a JSX child position lowers to an ifBlock when either arm is a JSX literal. The other arm's value — a keyed.map(…)array, a string, a variable holding an element, a nested ternary, a falsy primitive — was compiled into the branch helper as a bare expression statement, so its value was evaluated and dropped and the hole rendered empty. React renders that arm's value, and server rendering already did too, so the same component could ship content from the server that the hydrating client blanked out. Anullconsequent ({cond ? null : <Jsx/>}) did not compile at all: the lowering crashed reading the missing branch.A non-JSX arm now lowers to the authored-equivalent
<>{expr}</>, so the branch renders the value through the fragment's child hole: keyed.maparms take the same keyed fast path as@for, nested ternaries become nested ifBlocks, and primitives (including0) render as text. Anull/falseconsequent compiles to an ifBlock with an empty then branch, mirroring the long-supportednullalternate. The server now claims template-form ternary holes symmetrically (ssrControl+ arm ranges, exactly like an authored@if), so the hydrating client adopts the taken arm's DOM — including keyed list arms — instead of relying on shape coincidence. Value-form positions (returned.tsxtrees, whose holes the client folds into descriptor value holes) keep their existingssrChildoutput byte-for-byte.ef82ba3: Defer a held synchronous transition's whole commit.
A transition that suspends now holds the entire screen — including everything it patched outside the suspended boundary. Shell text, attributes, controlled form state and keyed structure revert with the hold,
isPendingstays on, and the new screen lands in one step when the data arrives, matching what async Actions already did. The composition benchmark records zero exposed intermediate states for a transition update, level with React.A value hole that leaves array mode during a held transition now re-asserts the held rows instead of showing the flipped-in content early, and a sole-child hole that flips array to text and back no longer crashes on the wiped list markers.
One residual is pinned at its own benchmark ceiling: the promoted round after a dependent request resolves re-creates warm-started fetches (served by the application cache, so nothing refetches over the network) until the follow-up resume work restores the exact creation floor.
0.1.20
30 Jul 2026c6370b6: Add
octane/testingwithclampJsdomScrollTop(), an opt-in helper that clamps jsdom's stored scroll position to its reported range and dispatches the resulting native scroll event.dd272ad: Expose a stable native error-boundary reset ref for renderer adapters.
c151b71: Add optional Strong mode for clearer state and ref behavior. Enable it across an application with
compiler: { strong: true }, in one module with"use strong", or through the Vite, Rspack, and Rsbuild plugin options. Strong modules reject state updates during render, direct state updates while setting up an effect, and render-time writes to refs, withuseLinkedStateavailable for state that should follow another value.
Patch Changes
c6370b6: Add
octane/testingwithclampJsdomScrollTop(), an opt-in helper that clamps jsdom's stored scroll position to its reported range and dispatches the resulting native scroll event.dd272ad: Expose a stable native error-boundary reset ref for renderer adapters.
c151b71: Add optional Strong mode for clearer state and ref behavior. Enable it across an application with
compiler: { strong: true }, in one module with"use strong", or through the Vite, Rspack, and Rsbuild plugin options. Strong modules reject state updates during render, direct state updates while setting up an effect, and render-time writes to refs, withuseLinkedStateavailable for state that should follow another value.66b51d8: Run the mount effects of components a boundary rendered but never committed.
A
useEffectcould be lost outright. When a Suspense boundary suspended on something rendered after a sibling — a@forof children followed by a component that callsuse()on a pending promise, say — those earlier siblings had already run their hooks before the attempt was abandoned. Their effects were queued and then dropped along with the rest of the aborted attempt, but the slots kept the dependency arrays that attempt had stamped. When the promise resolved and the content was revealed, the re-render compared those deps, found them unchanged, and enqueued nothing. The mount body never ran, and neither did the cleanup it would have returned, so subscriptions, timers, and observers set up inuseEffectsilently never started.useLayoutEffectwas unaffected.Hiding a boundary behind its fallback still leaves passive effects subscribed, which is what React does for content that is on screen and merely hidden. That now applies only to effects that actually ran. One that never ran has no subscription to preserve, so it resets like a layout effect and fires when the boundary finally commits — React fires every mount effect in the subtree when a suspended mount lands.
This also covers a boundary that commits, then re-renders with a new child and suspends: the new child's effects mount on reveal while its already-committed siblings keep the subscriptions they had.
a57c32a: Hold a Suspense boundary whole through a transition.
A transition that suspended used to leave part of the new screen on top of the old one. Rendering and mutating happen in one walk, so a component patched its own attributes and text on the way down and only afterwards found that a child below it was still loading — the boundary kept its old content but the markup around it had already moved on. The same thing happened when a held boundary replayed its body and only some of the data had arrived: the resolved parts committed and the rest stayed behind.
A suspended attempt now undoes its own binding writes, so the boundary either updates completely or not at all. The undo runs in the same flush as the change, so nothing intermediate is ever painted and transitions stay monotonic — no visible rollback, no invalid intermediate structure.
benchmarks/async-compositionrecords zero exposed intermediate states for a transition update, level with React, with its ceiling tightened from one to zero.Controlled
value,checkedandselectedare held too, along with theirdefault*mirrors and the record of what was last projected.Two things a transition can still change early: content it patched outside a suspended boundary, and a structural change above one such as a keyed list reordering.
e38a557: Hold a keyed list whole through a suspended transition.
A transition that dropped a row and then suspended used to lose the row from the held screen: the list reconciled first, the boundary decided to hold second, and by then the row's DOM, state and cleanups were already gone. A list the boundary was supposed to be holding frozen showed up with rows missing.
Removals inside a boundary now defer their teardown while a hold is still possible. The nodes come out of the way so the reconcile can finish, but they are kept, and the row's scope — its state, its effects, its cleanups — is left untouched until the outcome is known. If the boundary holds, the rows come back exactly as they were, cleanups never having run; once the transition commits, the removal goes through for real and cleanups fire then.
The list restores as a whole — order, membership and the
@emptybranch together — so reorders roll back alongside removals.bd90e27: Keyed-list parking only defers what a hold can restore.
A value-position keyed list whose slot leaves array mode (the value stops being an array) discards the slot itself, so rows removed by that flip have nothing to be restored into. They no longer park for a possible hold — their teardown runs inline with the attempt that removed them, exactly as it did before rows learned to wait — instead of being deferred past the rest of the render as unrestorable.
The rollback that puts a held list back also stops if a teardown cleanup flips the enclosing boundary to
@catchmid-restore, instead of writing into a disposed range.ae6811d: Add
useLinkedStatefor local state that can be edited independently but should reset or adjust when an input changes. The new value is available immediately, without an effect or a state update during render. Calculations can inspect the previous source and value, choose custom equality checks, and use the same optional latest-value getter asuseState.62d81b8: Guard
hot.datain the universal webpack-HMR handoff: webpack and rspack leavemodule.hot.dataundefined until a previous instance of the module has disposed, so the emittedhot.data.__octaneUniversalComponentsread crashed every dev bundle on its first evaluation.
0.1.19
29 Jul 20269d5d642: fix(compiler): keep
octane/compilerfree of Node builtinsoctane/compilerre-exported theoctaneVite plugin, which pullsnode:fs,node:path,node:crypto, andnode:moduleinto the subpath's module graph throughvite.jsandbundler.js. Bundled consumers never noticed — Vite and Rollup tree-shake the unused re-export — but consumers that import the subpath unbundled do: browsers and CDNs like esm.sh and jsdelivr resolve the whole graph, sooctane/compilerarrived with ~38KB of bundler code and Node polyfill shims attached, and had to be worked around with a deep path intodist/compiler/compile.js.The plugin keeps its two existing homes,
@octanejs/vite-pluginandoctane/compiler/vite, both of which still export it with types.import { octane } from 'octane/compiler'no longer resolves. Switch to:import { octane } from '@octanejs/vite-plugin'; // or import { octane } from 'octane/compiler/vite';f469b3f: Use the current TypeScript module-declaration
kindfield throughout Octane's compiler. Runtime client and server output continues to erase ambient global declarations, while editorto_tsoutput preservesdeclare globalas a valid, type-checkable global augmentation.ac2ae2f: Enable the existing keyed-list purity and automatic memoization optimizations for components authored with ordinary TSX/JSX returns. JSX lists now reuse unchanged items like equivalent TSRX templates while preserving context updates, captured values, component boundaries, and JSX children semantics. Native dense Arrays retain the optimized path; custom or overridden map methods, sparse Arrays, and additional map arguments preserve normal JavaScript behavior.
Patch Changes
9d5d642: fix(compiler): keep
octane/compilerfree of Node builtinsoctane/compilerre-exported theoctaneVite plugin, which pullsnode:fs,node:path,node:crypto, andnode:moduleinto the subpath's module graph throughvite.jsandbundler.js. Bundled consumers never noticed — Vite and Rollup tree-shake the unused re-export — but consumers that import the subpath unbundled do: browsers and CDNs like esm.sh and jsdelivr resolve the whole graph, sooctane/compilerarrived with ~38KB of bundler code and Node polyfill shims attached, and had to be worked around with a deep path intodist/compiler/compile.js.The plugin keeps its two existing homes,
@octanejs/vite-pluginandoctane/compiler/vite, both of which still export it with types.import { octane } from 'octane/compiler'no longer resolves. Switch to:import { octane } from '@octanejs/vite-plugin'; // or import { octane } from 'octane/compiler/vite';f469b3f: Use the current TypeScript module-declaration
kindfield throughout Octane's compiler. Runtime client and server output continues to erase ambient global declarations, while editorto_tsoutput preservesdeclare globalas a valid, type-checkable global augmentation.ac2ae2f: Enable the existing keyed-list purity and automatic memoization optimizations for components authored with ordinary TSX/JSX returns. JSX lists now reuse unchanged items like equivalent TSRX templates while preserving context updates, captured values, component boundaries, and JSX children semantics. Native dense Arrays retain the optimized path; custom or overridden map methods, sparse Arrays, and additional map arguments preserve normal JavaScript behavior.
3aada64: Keep JSX values in the render scope represented by their element tree, including implicit getter, Proxy, coercion, iterator, and computed-key evaluation. Preserve context, Suspense, error-boundary, SSR, hydration, and element-descriptor compatibility when JSX moves into a variable, prop, array, or another value position. Render deeply nested server component trees without exhausting the JavaScript call stack across buffered and streaming server-rendering APIs.
0.1.18
28 Jul 2026c3ba5e0:
compile({ inspect: true })now claims an authored attribute NAME for the tokens that name the call the attribute lowered to, so navigation tooling can resolve a dynamic attribute in both directions.A static attribute is baked into template markup and the template's origins already carried it. A dynamic one has no markup at all —
<form action={fn}>survives assetFormAction(el, 'action', …)/ssrAttr("action", …), and<input defaultValue={v}/>assetDefaultValueUncontrolled(el, v), which has no name token whatsoever — and every other token of those calls maps to the value expression. The authored name was therefore unreachable from the output: the helper is a different word and the name literal carries quotes, so neither reproduces the authored text a consumer accepts on an inferred attribution.Covers both emitters and every attribute that routes to a helper rather than to baked HTML: form actions, controlled and uncontrolled form props,
class,style,autoFocus, boolean, ARIA,data-*, and the generic attribute path.Inspection-gated as before — the emitted module is byte-identical with and without
inspect.430061e: Allow runtime host components to render descriptor children produced by TSX and
createElementcall sites.a21ff46: Tear down an effect when its conditional hook call site is not reached during a successful component render.
Patch Changes
c3ba5e0:
compile({ inspect: true })now claims an authored attribute NAME for the tokens that name the call the attribute lowered to, so navigation tooling can resolve a dynamic attribute in both directions.A static attribute is baked into template markup and the template's origins already carried it. A dynamic one has no markup at all —
<form action={fn}>survives assetFormAction(el, 'action', …)/ssrAttr("action", …), and<input defaultValue={v}/>assetDefaultValueUncontrolled(el, v), which has no name token whatsoever — and every other token of those calls maps to the value expression. The authored name was therefore unreachable from the output: the helper is a different word and the name literal carries quotes, so neither reproduces the authored text a consumer accepts on an inferred attribution.Covers both emitters and every attribute that routes to a helper rather than to baked HTML: form actions, controlled and uncontrolled form props,
class,style,autoFocus, boolean, ARIA,data-*, and the generic attribute path.Inspection-gated as before — the emitted module is byte-identical with and without
inspect.430061e: Allow runtime host components to render descriptor children produced by TSX and
createElementcall sites.a21ff46: Tear down an effect when its conditional hook call site is not reached during a successful component render.
1821f63: New compile option
dataCallbackHooks. A hook can now declare that a callback argument is data it memoizes on, rather than a dependency subject it re-runs. Production compiles then key that callback on the values it actually reads, so its identity moves only when they do.compile(source, filename, { dataCallbackHooks: ['@octanejs/tanstack-store#useSelector'], });Entries are
module#hookName, matched against the call site's import — including a namespace import, matched against the namespace's own module — or a barehookNamefor a hook declared in the module being compiled. A default or namespace import from elsewhere never matches a bare entry.This closes the other half of the capture-free callback lift. A callback that captures nothing is lifted to module scope and keeps one identity forever; a callback that reads component state cannot move, but is now wrapped in
useCallbackwith an inferred dependency list. On a 512-subscriber fan-out over 20 unrelated parent re-renders, selector invocations for a capturing selector drop from 10,240 to zero, and the re-render cost fell from 21.4–23.1ms to 7.8–9.9ms.The dependency list is left for inference to fill, which is why the transform runs before it. That ordering is load-bearing rather than incidental: coarse identifier dependencies (
[props]) are worthless here, because the props object is a fresh identity on every parent render, so the memo never hits. Inference produces the member paths actually read ([props.offset]).Nothing is inferred about which hooks qualify, and the transform is inert until something opts in. The compiler already refuses to attach dependency semantics to custom hooks it cannot statically prove, and a wrong answer here produces a stale closure — silent, and attributed to the application rather than to the compiler — so the fact is declared rather than guessed.
Declared hooks are still left alone where the hook owns freshness itself: when its own dependency list is inferred, and when the call passes an explicit dependency array or
null(the author's "re-run every render" escape hatch). Dev, HMR, and profile compiles keep the authored form, matching the neighbouring inline hook-memo tier.3db74e9: A directive at value position now compiles inside a callback and at module scope, so
@if/@for/@switch/@tryno longer depend on sitting directly in a component body.Inside a callback. A directive's arms are hoisted, so they cannot reach a callback's parameters lexically — but they never needed to. Captured values already travel through the construct's env channel, and that channel is built at the
createElement(_frag$N, …)call site, which sits inside the callback where its parameters are in scope. What was missing was the capture set: it held only the component's own locals, so a name introduced by an enclosing callback was dropped from the tuple and the arm was emitted reading it free — a module-scope helper closing over a variable that does not exist there, which only fails once the arm renders. The set now grows with each callback the value lowering descends through, and the same names ride an env array on the server, whose subs are declared in the owning body rather than hoisted. Nested callbacks compose: an arm may read from every enclosing scope.At module scope.
const v = @if (…) { … };has no component body to own it and needs none — it can only close over module bindings, which every hoisted helper already sees, so its arms hoist beside it. Both emitters fold it to their own shape; previously neither did, and the client's DOM helpers could be emitted into a server module.A module-level value is computed once, where it is written, exactly like
const v = cond ? <A/> : <B/>and like a React element built at module scope. The client already did that — its fold lifts the control expression out as a hole evaluated at the definition site — but the server compiles the directive into a sub it calls per render, so it re-read the expression every time. Given the same module state the two emitters could then disagree, and hydration reported nothing. The server now lifts the same expression (@if's test,@for's iterable,@switch's discriminant) to the definition site, so both freeze together.@tryhas no control expression: it selects an arm from what its body does at render time, which both emitters already did per render.What remains unsupported is a directive inside a MODULE-level callback: the env channel is built per component, and there is no component to build it against. That is now the only case the diagnostic covers, and its wording says so rather than claiming directives need an owning component in general.
Covered by client render, server render, hydration adoption, and type-only output, including a directive whose arm reads the parameter of an enclosing callback and one that reads through two nested callbacks.
0d4ed9e: Directives used at a VALUE position now compile on the client and the server. A
@if/@for/@switch/@tryis compiler-owned template syntax, but a value position lowers through the descriptor path rather than the template walk, and that path had no case for it. What happened next depended only on where the directive sat:const branch = @if (ok) { … } @else { … }emittedconst branch = {createElement(…)}— an object literal wrapping a call expression, so the module did not parse. Setup attribute values (<Child prop={@if …} />) had the same shape. The lowered descriptor was always wrapped in aJSXExpressionContainer, which is right only in JSX child position; anywhere else it prints as a bare{expr}block.<Child prop={@for …} />and<div>{@switch …}</div>in rendered output reached the printer as raw TSRX nodes and threwNot implemented: JSXIfExpression.<Child prop={<h1>@if … </h1>} />and<div>{<h1>@if … </h1>}</div>— a host element that is itself a value, holding a directive among its children — were compiled with the directive silently dropped. The element lowered to acreateElementdescriptor whose child lowering discarded every node it did not recognise, so the arms vanished with no diagnostic.
All of these now fold through the same hoisted-renderer path setup directive values already used, so each arm compiles once and the value becomes an ordinary renderable descriptor. Both emitters publish that fold for the body being compiled and restore the previous owner afterwards, so a nested body never folds its arms into its parent's helper list.
The fold stops at a callback boundary. A directive's arms are hoisted into the body that owns them and read the values that body threads in, so folding one that belongs to a callback would hoist arms referencing the callback's params into a scope where those params do not exist — a module-level helper closing over a free variable, which only fails once the arm renders. A value-position directive with no owning body is now a compile error naming the authored keyword and pointing at the fix (move the markup into its own component), rather than silently dropped markup or a helper that throws at runtime.
The type-only (
to_ts) emitter was already correct here; this brings the client and server emitters in line with it. Covers all four directives across the initializer, attribute-value, expression-container and element-holding-a-directive positions, in rendered output and in setup, with client render, SSR and hydration adoption tests.7bdf1fa: Correctly bind loop indexes in explicit TSRX and TSX list keys so indexed rendering and hydration do not throw.
e1927d8: Keep enclosing DOM boundaries attached when a ref-owning conditional branch switches to a lazy Suspense component.
dac0e66: Fixes two ways a loop head could hide a component reference from free-variable analysis, each of which let the capture-free hook-callback lift move a callback that was not actually capture-free:
A
for-in/for-ofhead with no declaration ASSIGNS an existing binding rather than introducing one. It was being treated as a fresh loop binding, sofor (acc of s.items) { … }hid the write to the component'saccentirely and the callback read as capture-free. Lifted to module scope, that assignment has no binding to land on.A destructuring default in a loop's declaration
for (const [x = props.seed] of s.pairs)— is an expression that runs on every iteration, but only the names the pattern declared were collected, so the read ofpropswas invisible.
Both are corrected in the shared analysis, so every caller of it benefits, and the same treatment is applied to the
@fortemplate directive's head.54c60fa:
compile({ inspect: true })now also claims the authored attribute NAME for the two lowerings that resolve an element's props as a GROUP, rather than one call per attribute. Both dropped the name on the way out, leaving common controlled form props unreachable from the output in the forward direction.A host with a spread, a duplicate prop, or a
value/defaultValuecascade routes every prop through one commit-phase collectorsetHostPropSources(el, [[false, 'defaultValue', …]])— where the per-source name literal is the only token that names its attribute. It now carries the authored name instead of the element's location, the same splitssrAttrs/ssrInputAttrsrows already made on the server.Server-side
<textarea>/<select>value/defaultValuenever serialize as attributes at all: they become the content-positionssrTextareaValue(…)or option-projectionssrSelectScope(…)call, which takes its writers POSITIONALLY, so there is no name literal and the helper alias is the only token there is. It is now anchored on the authored name, and when both writers are present the second aliases onto the first — one call cannot carry two locations.The emitted module is byte-identical with and without
inspect, and unchanged from before this fix.59a95d6: Production compiles lift a capture-free function argument of a hook call to module scope, so the hook sees one identity for the module's lifetime instead of a fresh function every render.
This matters for hooks that COMPARE the callback. A store selector is the motivating case:
useSelector(store, (s) => s.total)feeds a memo keyed on the selector's identity, and an inline arrow defeats it on every render — so an unrelated parent re-render re-runs the selection for every subscriber. Measured on 512 subscribers over 20 unrelated re-renders, selector invocations drop from 512 per render to zero. Holding the compile mode fixed and varying only selector identity, an expensive selector's re-render cost fell about 25% (62.6–64.1ms → 46.5–47.4ms across three runs); with a cheap selector the wall-clock difference stayed inside run-to-run noise, so the durable claim is the eliminated work rather than a fixed speedup.The analysis is deliberately conservative and over-approximates the component's bindings, so shadowing can only cost a lift, never produce a wrong one. A callback is left exactly where it was authored when it reads component state,
this, orarguments, renders JSX, or contains a hook call.Hooks whose contract is stated in terms of the callback are excluded, because moving it would change observable behavior rather than just an address:
useCallbackreturns the argument and owes a fresh identity when deps change;useMemoand the effect hooks are defined by how often the callback runs;useState/useReducertake a once-only lazy initialiser;useEffectEventdeliberately hands back an unstable wrapper. Customuse[A-Z]hooks anduseSyncExternalStore— whosesubscribe/getSnapshotare data it reads take the lift.Dev, HMR, and profile compiles keep the authored form, on the same gate as the neighbouring inline hook-memo tier.
Two free-variable analysis fixes come with it, both affecting capture analysis generally rather than only the lift:
A binding pattern's default initialisers and computed keys are expressions evaluated at binding time, and were never walked — only the names a pattern declared were collected. So
(s, scale = props.factor) => …and({ [props.field]: picked }) => …reported no reference topropsat all, and read as capture-free.A classic
for (let i = 0; …)carries its declaration in the loop'sinitrather than itsleft, soiwas reported free and read as a capture of an enclosing binding the loop actually shadows. This made any callback containing a counting loop look instance-specific.
138fbd9: Compiler-inferred hook dependency arrays now resolve two capture-analysis defects.
A computed key in a binding pattern (
const { [key]: picked } = source) is a read of the surrounding scope, but the scope walk never visited it, so the identifier resolved to no binding at all — indistinguishable from a global and was dropped. Any effect keyed on that value kept a stale capture and never re-ran when it changed. The fix covers every position a pattern can appear in: declarations, function parameters, catch clauses, for-of heads, and patterns nested in any of them.Module-scope
constdeclarations, and module-scopefunctionandclassdeclarations that nothing reassigns, are no longer emitted as dependencies. A dependency array tracks what can change between renders, and these are evaluated once for the program's lifetime — the conclusion imports already got here, thatexhaustive-depsreaches for any outer-scope value, and that autoMemo already reached for same-module function declarations. Previouslyimport { fmt } from './fmt'and a byte-identical local helper produced different arrays. A localconstthat only names such a value, or that binds a literal, is omitted for the same reason.Module-scope
letandvarremain tracked, since any later statement may rebind them. A member read through a module-scopeconst(CONFIG.mode) is now omitted, matching the answer a namespace import already gave: a module-level object mutated in place is not witnessed by a dependency array either way.A regex literal is not treated as an invariant initializer. ESTree spells
/foo/gas aLiteral, but it allocates a fresh RegExp on every evaluation and carries mutablelastIndexstate, so a localconst pattern = /foo/gstays reactive. The predicate is now shared with the onecompile.jsalready used for the same question.50c1ab5: Preserve independently managed streamed DOM when it is interleaved with renderer-owned host children.
The de-optimized host reconciler now adopts unstamped nodes only during hydration. Normal client updates retain external stream boundaries, list nodes, interactive controls, and their listeners while continuing to update, reorder, and remove children created by Octane itself.
e0c5490: Clear a shared-parent keyed list by walking its marker span, falling back to the scoped Range only once the list is large enough to pay for it.
A
@forblock that owns its parent is still cleared withtextContent = ''. One that shares its parent with other JSX previously always usedRange.deleteContents(). Measured in Chromium, that is the slower of the two for every list size a page realistically clears — 2.2× at 10 items, 1.3× at 100 — and only pulls ahead by ~8-12% past roughly a thousand items, so the strategy is now chosen by item count.The gap is far wider off the browser. jsdom decides Range containment with a boundary-point comparison per candidate node, each of which can walk the whole document, making
deleteContentscost O(items × document): 266× the walk when clearing 100 items from a 3400-node document, growing with PAGE size rather than list size. Any Octane component test that clears such a list paid it one route in this repo's own suite spent 2.8-7.3s in a single render, against ~150ms for the same component tree mounted directly, and now renders in ~200ms.No change for the owns-parent case, which is what the existing
clearbenchmarks exercise — every other suite's@foris the sole child of its parent, so the shared-parent path had no coverage at all. The newlist-clearsuite adds it, with both sizes and an owns-parent control, and gates each op on the neighbours surviving the clear (a clear that took the wrong span would otherwise report as a faster one).e6a158e: A hook callback written through a type assertion —
((s) => s.total) as Selis now recognised as a callback. It previously matched neither the module-scope lift nor dep-keying, because both tested the argument's node type directly and aTSAsExpressionis not an arrow, so a typed selector silently kept its per-render identity churn.Both passes now peel the assertion, and both move the UNWRAPPED function. The assertion is erased from the emitted module either way, and dropping it means a type alias declared inside the component cannot be dragged out to module scope by the lift.
0.1.17
27 Jul 2026bd31a2d: Derived values are now cached at their declaration. A
constwhose initializer performs a call during render — where every such call is a value projection by the rule automatic memoization already uses — is lowered to a compiler-owned memo keyed on the component locals it reads, so its identity is stable until those inputs change.This is what makes region memoization worth having. A region keys on the identity of what it renders, so a derived value rebuilt on every render defeats its cache unconditionally: memo-wall's value-position wall rebuilt 1,000 descriptors through
buildValueRows(items)on every parent re-render, and that call alone was 35% of the operation's CPU profile.A calculation is admitted on exactly the callee rule that governs regions, so the two agree. A member call is not cached even when its result is named:
virtualizer.getVirtualItems()moves with scroll while the virtualizer instance stays put, and caching it froze a virtualized list mid-scroll. That also meansconst visible = todos.filter(...)stays uncached — a genuine miss, sincetodosreally is an immutable snapshot, but this analysis cannot yet tell an immutable receiver from a live one. Wrap it inuseMemoyourself when the identity matters; recovering it automatically needs receiver provenance and is left as follow-up.Also never cached: hook calls (recognised by naming convention, including React's
unstable_use*staging prefix — a cache around a hook freezes its state cell and any subscription it owns),letdeclarations, which stay the escape hatch for a value that must recompute every render, and calculations the render tree never reads. Server compiles are untouched, since a server render evaluates each body once.Measured on
benchmarks/memo-wall, paired runs against a baseline recorded before any edit:parent_rerender_equal_B 0.226ms → 0.091ms (−60%) one_change_B 0.235ms → 0.156ms (−34%) ctx_through_wall_B 0.569ms → 0.413ms (−27%)
That is the
createElement-descriptors-through-a-children-hole shape every@octanejs/*binding produces.todomvc,chat-streamandjs-frameworkcompile byte-identically with and without this change and are reported as controls only — their run-to-run movement (up to ±100% on sub-millisecond operations) is the noise floor, not a result.Compiled output grows 0.07% gzip on the
codegen-sizecorpus.9e0ef45: Stop reporting a deleted subtree's cleanup as a render-phase cross-component update.
Octane discovers a deletion while reconciling the deleting parent's output, so
CURRENT_BLOCKstill names that parent while the removed subtree's destroys and cleanups run. A cleanup that callssetStateon a surviving component was therefore treated as a render-phase update: it loggedCannot update a component (X) while rendering a different component (Y)and flagged the target for the render-phase branch of the update-depth error.Those callbacks are mutation-phase work (React runs them in
commitDeletionEffects), and updating another component from them is the normal registry pattern, so they no longer take that branch.@octanejs/radix's Select hit this on every open and close:SelectItemText's layout cleanup callsonNativeOptionRemoveon the Select provider as the content swaps between its detached fragment and the open popper.Genuine render-body updates are unaffected: those run with no cleanup on the stack and still warn and still terminate the loop.
dea219b: Stop the de-opt reconciler removing DOM it did not create.
An element rendered with no children still had its existing DOM reconciled against an empty child list, so anything inserted by other means — a
replaceChildrenwith a captured snapshot, a third-party widget mounted into a container — was swept away by the next unrelated re-render. React only manages children it created, and portal ranges were already excluded here for that reason; this extends the same treatment to imperative content.Children the renderer did commit still clear when they go away, so
{cond && <X/>}is unaffected.Found from
@octanejs/base-ui, whose popup Viewport fills a snapshot container this way and lost its previous content mid-transition.
Patch Changes
bd31a2d: Derived values are now cached at their declaration. A
constwhose initializer performs a call during render — where every such call is a value projection by the rule automatic memoization already uses — is lowered to a compiler-owned memo keyed on the component locals it reads, so its identity is stable until those inputs change.This is what makes region memoization worth having. A region keys on the identity of what it renders, so a derived value rebuilt on every render defeats its cache unconditionally: memo-wall's value-position wall rebuilt 1,000 descriptors through
buildValueRows(items)on every parent re-render, and that call alone was 35% of the operation's CPU profile.A calculation is admitted on exactly the callee rule that governs regions, so the two agree. A member call is not cached even when its result is named:
virtualizer.getVirtualItems()moves with scroll while the virtualizer instance stays put, and caching it froze a virtualized list mid-scroll. That also meansconst visible = todos.filter(...)stays uncached — a genuine miss, sincetodosreally is an immutable snapshot, but this analysis cannot yet tell an immutable receiver from a live one. Wrap it inuseMemoyourself when the identity matters; recovering it automatically needs receiver provenance and is left as follow-up.Also never cached: hook calls (recognised by naming convention, including React's
unstable_use*staging prefix — a cache around a hook freezes its state cell and any subscription it owns),letdeclarations, which stay the escape hatch for a value that must recompute every render, and calculations the render tree never reads. Server compiles are untouched, since a server render evaluates each body once.Measured on
benchmarks/memo-wall, paired runs against a baseline recorded before any edit:parent_rerender_equal_B 0.226ms → 0.091ms (−60%) one_change_B 0.235ms → 0.156ms (−34%) ctx_through_wall_B 0.569ms → 0.413ms (−27%)
That is the
createElement-descriptors-through-a-children-hole shape every@octanejs/*binding produces.todomvc,chat-streamandjs-frameworkcompile byte-identically with and without this change and are reported as controls only — their run-to-run movement (up to ±100% on sub-millisecond operations) is the noise floor, not a result.Compiled output grows 0.07% gzip on the
codegen-sizecorpus.9e0ef45: Stop reporting a deleted subtree's cleanup as a render-phase cross-component update.
Octane discovers a deletion while reconciling the deleting parent's output, so
CURRENT_BLOCKstill names that parent while the removed subtree's destroys and cleanups run. A cleanup that callssetStateon a surviving component was therefore treated as a render-phase update: it loggedCannot update a component (X) while rendering a different component (Y)and flagged the target for the render-phase branch of the update-depth error.Those callbacks are mutation-phase work (React runs them in
commitDeletionEffects), and updating another component from them is the normal registry pattern, so they no longer take that branch.@octanejs/radix's Select hit this on every open and close:SelectItemText's layout cleanup callsonNativeOptionRemoveon the Select provider as the content swaps between its detached fragment and the open popper.Genuine render-body updates are unaffected: those run with no cleanup on the stack and still warn and still terminate the loop.
dea219b: Stop the de-opt reconciler removing DOM it did not create.
An element rendered with no children still had its existing DOM reconciled against an empty child list, so anything inserted by other means — a
replaceChildrenwith a captured snapshot, a third-party widget mounted into a container — was swept away by the next unrelated re-render. React only manages children it created, and portal ranges were already excluded here for that reason; this extends the same treatment to imperative content.Children the renderer did commit still clear when they go away, so
{cond && <X/>}is unaffected.Found from
@octanejs/base-ui, whose popup Viewport fills a snapshot container this way and lost its previous content mid-transition.2374980: Allocate a Scope's
cleanupsandchildrenarrays lazily.Both were created eagerly for every Scope even though most scopes never register a cleanup or a child scope, while the neighbouring
hooks,effectSlotsand_slotsfields were already lazy. They now follow the same pattern:nulluntil something registers, allocated at the registration site.On a 500-row × 3-cell tree (2,501 scopes) this removes 4,002 of the 7,503 array allocations those three collections were making — every
cleanupsarray and 60% of thechildrenarrays. Compiled output grows 17 bytes gzipped across the 16-file codegen corpus, from the??=at the three cleanup-registration sites.slotsdeliberately stays eager: every scope in that same measurement used it, so making it lazy would add a null check to the framework's hottest path and to every emitted__s.slots[N]access while saving nothing.2374980: Fix a context Provider corrupting the tree when its children switch dialect.
A Provider accepts its children either as the compiled children-block function a
.tsrxparent passes, or as an element descriptor from acreateElementparent. Both claimedscope.slots[0]— a compiled body stores its binding bag there, while the descriptor path stores achildSlotrecord — so a parent that wraps its children conditionally, and therefore alternates between the two shapes across renders, had the incoming dialect read the outgoing one's record as its own. The result was aTypeErrorand a detached subtree.The children now remount across such a flip, which is the same contract React gives an element-type change.
ac687f8: The development controlled-form diagnostic no longer warns on
aria-hiddeninputs. An aria-hidden control is the hidden form-interop "mirror input" pattern (e.g. the bubble inputs radix-style libraries render behind a custom control): it is assistive-technology-hidden and focus-excluded, so handler-less controlledchecked/valueprops are the intended wiring there, not the authoring mistake the diagnostic exists to catch. Real, user-reachable controls keep the full warning.7997d39: Speed up the server's async-identity string encoding. Identity scopes encode each UTF-16 code unit at a fixed width so lone surrogates stay injective, but the encoder built every unit with
toString(16).padStart(4, '0')— two throwaway string allocations per character, which showed up as roughly 15% of render time on descriptor-heavy SSR (the shape@octanejs/*bindings produce). Identity keys are overwhelmingly ASCII, so those units now come from a prebuilt table and only the rare non-ASCII unit takes the formatting path.The emitted encoding is unchanged: verified byte-identical across all 65536 code units, lone surrogates, and fuzzed inputs.
eb69cb6: Authored
<title>/<meta>/<link>now reach the real<head>in file-routed SSR apps. The route renders into the template's<div id="root">, not a document, so core's head fold had no</head>to target and prepended the metadata inside#rootinstead: the template's<title>won by document order,link rel="canonical"andmeta name="description"were ignored where they landed, and hydration could not find the ownership markers indocument.headso it appended duplicates.New opt-in
RenderOptions.headChannel: 'separate'withholds hoisted metadata fromhtml/the streamed shell and hands it over on its own, throughRenderResult.headfor the buffered renderers and the newStreamOptions.onHeadReady(head)for the streaming ones (called before the shell is written, so a host can still place it in the template prefix). Both the dev server and the production handler use it and splice at<!--ssr-head-->.The default stays
'fold'and is unchanged: same bytes, same result shape, noheadfield. Core does not dedupe metadata, so a<title>inindex.htmland one in a component both still ship.
0.1.16
26 Jul 202685a1c6d: The auto-memo analysis no longer walks each component body twice to collect the imported components it renders and to look for a deferred ref read. Both questions are answered by one traversal, halving the node visits and visited-set allocations those two passes cost. Compiler output is unchanged.
f4c97d8: Extend the opt-in compiler inspection surface (
compile(source, file, { inspect: true })) so source↔output navigation reaches constructs that leave no trace of themselves in the emit.result.inspect.segmentsentries now carryexact, marking a segment the compiler itself anchored on an authored span rather than one inferred from the print's map; control-flow directive keywords (@if,@else,@for,@empty,@switch,@case,@default,@try,@pending,@catch), event-attribute names, and scoped<style>blocks claim the code they lowered to through it.result.inspect.templatesgains SSR entries (each static run's exact bytes plus its origins) andresult.inspect.aliasesrelates an authored span to the origin that owns its emission.octane/compiler/volaraddscompileTypesInspection, a navigation-only sibling ofcompileToVolarMappings: same parse, same transform, same output bytes, without the Volar mapping layer, so nothing here can perturb the language server. Emitted code and source maps remain byte-identical with the option on or off, and normal compiles skip all recording.f3543bf: The compiler's copy-on-write AST rewriter no longer recurses into properties that cannot be rewritten. Only object-valued properties can produce a replacement, but every
type,nameandrawstring was still passed to a recursive call that returned it unchanged, which was 61% of the walk. Compiles are measurably faster with byte-identical output.
Patch Changes
85a1c6d: The auto-memo analysis no longer walks each component body twice to collect the imported components it renders and to look for a deferred ref read. Both questions are answered by one traversal, halving the node visits and visited-set allocations those two passes cost. Compiler output is unchanged.
f4c97d8: Extend the opt-in compiler inspection surface (
compile(source, file, { inspect: true })) so source↔output navigation reaches constructs that leave no trace of themselves in the emit.result.inspect.segmentsentries now carryexact, marking a segment the compiler itself anchored on an authored span rather than one inferred from the print's map; control-flow directive keywords (@if,@else,@for,@empty,@switch,@case,@default,@try,@pending,@catch), event-attribute names, and scoped<style>blocks claim the code they lowered to through it.result.inspect.templatesgains SSR entries (each static run's exact bytes plus its origins) andresult.inspect.aliasesrelates an authored span to the origin that owns its emission.octane/compiler/volaraddscompileTypesInspection, a navigation-only sibling ofcompileToVolarMappings: same parse, same transform, same output bytes, without the Volar mapping layer, so nothing here can perturb the language server. Emitted code and source maps remain byte-identical with the option on or off, and normal compiles skip all recording.f3543bf: The compiler's copy-on-write AST rewriter no longer recurses into properties that cannot be rewritten. Only object-valued properties can produce a replacement, but every
type,nameandrawstring was still passed to a recursive call that returned it unchanged, which was 61% of the walk. Compiles are measurably faster with byte-identical output.dfa6d29: Server rendering no longer emits a style declaration whose value serializes to nothing.
style={{ color: '' }}producedstyle="color:;"on the server while the client produced no style attribute at all, so the markup could not be hydrated and the element was rebuilt. Empty, whitespace-only and empty-after-unit-handling values are now skipped, matching the client and React.9fbf31a: Cut the per-render cost of value-position element descriptors, the shape every
@octanejs/*binding and everycreateElement/.map()child tree produces.createElementno longer allocates a property descriptor per call to detect React's DEV-onlykeywarning getter (that probe is now DEV-gated exactly as in React'shasValidKey), a keyed element no longer pays a WeakSet insert to record key presence that its non-nullkeyalready proves,prepareDeoptListbuilds its output arrays only once a list regime is established, and every renderable hole no longer re-reads its host's tag from the DOM to re-check a void-element constraint its enclosing list already validated.On the
memo-wallbenchmark (1000memo(Row)children reached through a{rows}children hole) this drops a parent re-render absorbed by 1000 prop bails from 0.272ms to 0.177ms, a single-row change amid the wall from 0.275ms to 0.184ms, and a context bump through the wall from 0.610ms to 0.517ms. Exact render counts and compiled-work counts are unchanged, andmemo-wallnow carries ratio guards for all three wall-B operations.The server runtime gets the same two allocation fixes that apply to it (the key probe and the deferred child-list arrays) so the client and server descriptor paths stay in step;
ssr-throughputshows no measurable change from them.
0.1.13
22 Jul 2026Reading the note…
0.1.12
20 Jul 2026Reading the note…
0.1.11
19 Jul 2026Reading the note…
0.1.10
18 Jul 2026Reading the note…
0.1.9
18 Jul 2026Reading the note…