KUDZU DOCUMENTATION · v0.17.1
Write TSX.
Ship HTML.
Kudzu treats React-shaped TSX as compiler input, specializing it into complete HTML and only the route-specific browser capabilities used. There is no virtual DOM, hydration pass, or client component tree.
GETTING STARTED
Installation
Kudzu requires Node.js 22 or newer. Create a project and start the development server:
npm create kudzu@latest my-app
cd my-app
npm install
npm run devTo add Kudzu to an existing project, install @kudzujs/core and typescript, configure TypeScript with jsxImportSource: "@kudzujs/core", and add these scripts to package.json:
npm install @kudzujs/core@^0.17.1 typescript{
"scripts": {
"dev": "kudzu dev",
"build": "kudzu build",
"check": "tsc --noEmit && kudzu build"
}
}GETTING STARTED
Pages & routes
Every TSX file in src/pages becomes static HTML in dist.
src/pages/index.tsx→/src/pages/docs.tsx→/docssrc/pages/blog/index.tsx→/blogBracket parameters use getStaticPaths() to emit multiple static pages with build-time props.
// src/pages/posts/[slug].tsx
export async function getStaticPaths() {
return [
{ params: { slug: "oak" }, props: { title: "Oak" } },
{ params: { slug: "pine" }, props: { title: "Pine" } }
]
}
export default function Post({ title }: { title: string }) {
return <h1>{title}</h1>
}Missing params, unsafe path segments, and duplicate output routes fail the build. Catch-all parameters are not yet supported.
Runtime parameters
When a bracket value exists only in the request URL, export runtimeParams = true and read it with useParams(). Kudzu emits one static fallback document and a route-specific pathname matcher, not a client router.
// src/pages/items/[id].tsx
import { useEffect, useParams } from "@kudzujs/core"
export const runtimeParams = true
export default function ItemPage() {
const { id } = useParams<{ id: string }>()
useEffect(() => {
fetch(`/api/items/${encodeURIComponent(id)}`)
}, [])
return <h1>Item {id}</h1>
}getStaticPaths() and runtimeParams are mutually exclusive. Parameters occupy complete path segments and malformed, separator, control, and traversal-like values are rejected. Static hosts must try exact files first and then internally rewrite matching paths to the generated fallback while preserving the URL. Ordered rewrite metadata is available to afterBuild().
Migration input may retain a named or aliased useParams import from react-router-dom in this runtime route shape. Kudzu redirects a direct zero-argument call to the same pathname reader and removes the router package import. New Kudzu source should continue importing useParams from @kudzujs/core. Build-known getStaticPaths() routes receive their values through page props instead.
A named or aliased React Router useMatch may directly initialize one top-level const from an exact static root-relative string such as useMatch("/") in route scope. Kudzu evaluates it case-insensitively from each build-known application route, so a reused component can select route-specific static output without a browser router or JavaScript. Layout use, runtime-parameter pages, params, wildcards, query/hash patterns, trailing slashes, dynamic patterns, and indirect calls remain unsupported.
Query parameters
Migration source may retain a named or aliased React Router useSearchParams call. One top-level tuple binding and direct static get() locals become nullable route signals backed by a minimal URLSearchParams capability. The exact pagination form Number(params.get("page")) || 1 and a string fallback from a static element of a named relative immutable array reuse that signal and the primitive binding evaluator. An optional setter accepts direct inline updater calls from browser callbacks.
import { useSearchParams } from "react-router-dom"
export default function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams()
const query = searchParams.get("q")
return <button data-query={query} onClick={() => setSearchParams(previous => {
const next = new URLSearchParams(previous)
next.set("q", "next")
return next
}, { replace: true })}>{query}</button>
}The static fallback renders missing query values as blank text and omits nullable attributes. Browser initialization uses native decoding semantics before route effects mount. Setter calls without options use history.pushState(); exactly { replace: true } uses history.replaceState(). Both recommit query signals immediately, and browser back/forward follows popstate. Dynamic names or indexes, direct-value setters, other options, getAll, has, aliases, broader fallback expressions, and indirect reads are rejected. Routes without query reads or writes emit no parameter module.
React Bootstrap layout
Migration source may retain named or aliased React Bootstrap Row and Col elements with children, a static className, and numeric literal Col spans from 1 through 12 for xs through xxl. Kudzu emits native Bootstrap grid markup with no browser JavaScript. Keep Bootstrap CSS reachable from the route; boolean/object/dynamic grid props, dynamic classes, spreads, and other React Bootstrap components remain unsupported.
Application navigation
Native anchors are the default. Migration input may retain a named or aliased React Router Link when to is one static root-relative path; Kudzu applies the configured base, emits an ordinary anchor, and removes the package import.
import { Link as RouterLink } from "react-router-dom"
<RouterLink to="/products?sort=name#results" className="nav-link">
Products
</RouterLink>
// With base: "/app", emits:
// <a href="/app/products?sort=name#results" class="nav-link">Products</a>Dynamic or relative destinations, NavLink, router-only props, spreads, default or namespace imports, and non-JSX uses are rejected rather than approximating React Router semantics. Ordinary authored <a href> values remain unchanged.
Imperative migration source may use one top-level named or aliased useNavigate() binding. Direct calls inside browser callbacks lower to native document navigation after Kudzu applies the configured base.
import { useNavigate } from "react-router-dom"
export function CheckoutButton() {
const navigate = useNavigate()
return <button onClick={() => navigate("/checkout")}>Checkout</button>
}A one-argument call uses location.assign(); exactly { replace: true } uses location.replace(). Destinations must be safe static root-relative strings. Dynamic and relative destinations, render-time calls, passed aliases, and other options are rejected. This remains native document navigation rather than an SPA router.
Emitted exact and runtime-parameter routes may share a page-exported layout and opt into same-document navigation while every URL remains a complete standalone document.
// src/pages/product.tsx
export { Shell as layout } from "../components/Shell"
export default function ProductPage() {
return <main><h1>Product</h1></main>
}
// kudzu.config.mjs
export default {
navigation: { routes: ["/product", "/items/[id]"] }
}// Multiple shared layouts (mutually exclusive with routes)
export default {
navigation: { groups: [
{ routes: ["/product", "/items/[id]"] },
{ routes: ["/account", "/settings"] }
] }
}Every configured pattern must identify a globally unique emitted route. A runtime route uses its bracket pattern, such as /items/[id]. Each group uses one page-exported layout identity, while different groups may use different layouts. Kudzu emits one deterministic route-hashed, capability-specialized asset per group and rejects overlapping path domains across groups. Layout DOM, state, and effects persist within a group; route state, parameters, effects, and DOM-owner records reset after cleanup. Conditional and keyed effects follow those layout or route lifetimes, with fresh route records and item-property subscriptions on cached revisits. Eligible same-group anchors prefetch validated complete documents. Cross-group links, direct loads, reloads, failures, malformed paths, unsupported links, and ungrouped routes keep native document navigation. Coordinated exit/shared-element View Transitions are not supported.
Project configuration
// kudzu.config.mjs
export default {
base: "/newsletter",
publicDir: "../public",
styles: [{ source: "../src/styles/global.css", output: "/assets/styles.css", transform: css => transformCss(css) }],
metadata: ({ props }) => ({ lang: props.locale, manifest: "/manifest.json" }),
async afterBuild({ outDir, routes, plans, rewrites, base }) {
// Write host rewrites, RSS, sitemap, or search indexes.
}
}The base prefixes runtime, handler, stylesheet, icon, manifest, lowered React Router Link, and dev-server URLs without nesting files under dist. Ordinary authored anchor URLs remain under application control. Relative source CSS imports follow each page's reachable TypeScript import and re-export graph, emit under dist/assets, and link only from those routes in deterministic order. Declare truly global styles through kudzu.config styles. Enhanced navigation loads destination CSS before replacing route DOM, retains shared layout links, and removes outgoing route links. Source style entries may transform CSS before writing their declared output. publicDir defaults to public. Config and page metadata may be objects or functions of route props; page metadata wins. Reserve afterBuild() for host rewrites and extra artifacts rather than HTML or stylesheet mutation. Page JSX must not render body stylesheets.
Trusted HTML
<article dangerouslySetInnerHTML={{ __html: renderedNotionHtml }} />Raw HTML is not sanitized. It accepts trusted build-time content only; reactive values, children on the same element, void elements, and keyed-list raw HTML are rejected.
CORE
Components
Ordinary common React-shaped TSX should migrate with minimal source restructuring. Use familiar function components, props, children, fragments, collections, hooks, and conditions; Kudzu prefers compiler specialization over imperative DOM rewrites. This product direction is not specific to one migrated application. Components run at build time and do not remain as a browser-side tree.
Migration source may retain conventional react imports for supported named or aliased hooks, direct members such as React.useState, same-file memo, inline useCallback, primitive/direct-state expression or analyzable collection-pipeline useMemo, direct intrinsic forwardRef, top-level const identifiers initialized by useId(), and default, namespace, or named Fragment. A named or default zero-argument custom hook imported from a relative TypeScript module may expose direct shorthand useState value/setter pairs and callbacks that capture those states, including direct multi-state literal reset actions; the caller uses one top-level const object destructuring without aliases, defaults, or rest. One relative useDebounce(state, literalDelay) shape may instead return a primitive state directly when its single timeout dependency effect has exact cleanup. A direct React createRef() may attach once to an intrinsic element and enter one relative outside-click hook with an inline literal setter callback and matching document-listener cleanup. A supported forwardRef uses one inline synchronous (props, ref) function and forwards the object ref exactly once to its direct intrinsic root; Kudzu removes ref from props/rest and erases the wrapper. Kudzu turns useId() into a deterministic static HTML ID with no browser capability; keyed rows key-scope direct intrinsic ID and ID-reference attributes through existing list ownership by row key. Collection memos may start from local array state or a named relative import of an exported JSON-safe const array and filter by declared direct local-state dependencies. One direct array field from a top-level relative calculation result may also feed a keyed intrinsic map; route binding ESM refreshes the existing keyed list path after source-state commits. Kudzu canonicalizes these forms before evaluation; memo wrappers become build-time components, callbacks, direct bindings, or keyed-list selectors rather than a browser cache. React is not loaded or emitted, and a static route remains JavaScript-free.
Structural keyed rows over compiler-owned static filters use a route-specific fast path: source items and keys validate once, retained rows never move, and filtered rows restore as fresh clones of detached prototypes. The historical 0.7.8 matched snapshot measured Kudzu at 8.4 ms to filter and 3.6 ms to restore under 4x CPU throttling; its local runner and raw arrays are not tracked in this repository. Large datasets should still be paginated or windowed rather than rendered as 100,000 or 1,000,000 DOM nodes.
function Greeting({ name }: { name: string }) {
return <h1>Hello {name}</h1>
}
export default function Page() {
return <Greeting name="Kudzu" />
}Native dialog migration
React UI packages that depend on portals or a retained Context tree do not run in Kudzu. Migrate the used component boundary to a relative component backed by the native platform instead. A shadcn/Radix-shaped dialog can retain forwardRef, props, children, object refs, and ordinary handlers while replacing the package-owned Portal and Context with <dialog>.
type Props = {
"aria-describedby": string
"aria-labelledby": string
children?: unknown
onCancel: (event: Event) => void
}
const DialogContent = forwardRef<HTMLDialogElement, Props>(
({ children, ...props }, ref) => <dialog {...props} ref={ref}>{children}</dialog>
)
function ProfileDialog() {
const dialogRef = useRef<HTMLDialogElement>(null)
const triggerRef = useRef<HTMLButtonElement>(null)
return <>
<button ref={triggerRef} onClick={() => dialogRef.current?.showModal()}>Open</button>
<DialogContent ref={dialogRef} aria-labelledby="profile-title" aria-describedby="profile-description" onCancel={event => {
event.preventDefault()
dialogRef.current?.close()
triggerRef.current?.focus()
}}>
<h2 id="profile-title">Edit profile</h2>
<p id="profile-description">Update your public profile.</p>
<button onClick={() => {
dialogRef.current?.close()
triggerRef.current?.focus()
}}>Close</button>
</DialogContent>
</>
}Kudzu emits the complete dialog HTML and only the route's native event handlers. The application must preserve labeling and focus restoration as shown. Radix package execution, Portal, asChild/Slot, element cloning, and arbitrary compound-component Context remain unsupported; this is a source migration recipe, not package compatibility.
Native form migration
React Hook Form does not run in Kudzu. For ordinary forms, migrate its registration and submit wrapper to native controls, constraint validation, and one direct submit handler. Keep uncontrolled field values in the DOM and reserve application state for submitting, server errors, and success feedback.
const [status, setStatus] = useState("idle")
async function submit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
event.preventDefault()
const email = String(new FormData(event.currentTarget).get("email") ?? "")
setStatus("submitting")
const response = await createAccount(email)
setStatus(response.ok ? "success" : "error")
}
return <form onSubmit={submit}>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
required
aria-invalid={status === "error" ? "true" : "false"}
aria-describedby={status === "error" ? "signup-error" : undefined}
/>
<button disabled={status === "submitting"}>Create account</button>
{status === "error" && <p id="signup-error" role="alert">Email already registered.</p>}
</form>Read event.currentTarget and construct FormData before the first await. Native required, input types, lengths, and patterns handle synchronous field validity without input listeners. Kudzu emits complete form HTML, one route-specific submit handler, and only the bindings and conditional feedback used. useForm, register spreads, handleSubmit, Controller, watchers, resolver packages, dirty/touched proxies, and dynamic field registration remain source migration work rather than supported React Hook Form runtime APIs.
Data-fetching library migration
TanStack Query does not run in Kudzu. Classify each read by when its inputs exist. Build-known data belongs in an async page or component and becomes complete zero-JavaScript HTML. Data available only in the browser belongs in an inline effect with application-owned loading, error, and result state.
// Build-known data
export default async function ProductsPage() {
const products = await loadProducts()
return <ul>{products.map(product => <li key={product.id}>{product.name}</li>)}</ul>
}
// Browser-only data
const [request, setRequest] = useState(0)
const [status, setStatus] = useState("loading")
const [products, setProducts] = useState([])
useEffect(() => {
setStatus("loading")
void fetch("/api/products?request=" + request)
.then(response => response.json())
.then(next => {
setProducts(next)
setStatus("success")
})
}, [request])
return <button onClick={() => setRequest(request + 1)}>Refetch</button>Dependency replacement invalidates the previous effect invocation before cleanup, so late promise or fetch setters cannot overwrite newer state. A primitive request counter provides explicit refetch without a query runtime. Applications still own HTTP error handling and cleanup for imperative resources. Query clients, Providers, caches, retries, deduplication, optimistic cache updates, query-key arrays, Suspense, and background refetch remain source migration work.
Icon library migration
React icon packages such as Lucide do not run during Kudzu rendering. Move only the icons an application uses into small relative TSX components with direct SVG roots. Familiar props remain ordinary component inputs and React-shaped SVG attributes normalize to native output.
function SearchIcon({ title, ...iconProps }: IconProps) {
return <svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
{...iconProps}
>
{title && <title>{title}</title>}
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</svg>
}
return <>
<SearchIcon role="img" title="Search catalog" />
<CheckIcon aria-hidden={true} />
</>Meaningful icons need an explicit accessible name, while decorative icons should remain hidden from assistive technology. Kudzu emits the used SVG markup directly into complete HTML, removes build-folded evaluator artifacts, and never compiles unreachable icon modules. lucide-react execution, createLucideIcon(), dynamic icon-name lookup, package factories, and a generic icon runtime remain unsupported.
Scroll-spy migration
A Memos-shaped outline can keep its ordinary effect-owned animation-frame ref. Kudzu moves the exclusive useRef(0) value into the effect invocation closure rather than serializing it or shipping an animation runtime.
const [activeSlug, setActiveSlug] = useState<string | null>(null)
const rafRef = useRef(0)
useEffect(() => {
const update = () => {
rafRef.current = 0
const active = headings.findLast(heading =>
document.getElementById(heading.slug)!.getBoundingClientRect().top <= 100
)
setActiveSlug(active?.slug ?? null)
}
const requestUpdate = () => {
if (!rafRef.current) rafRef.current = requestAnimationFrame(update)
}
update()
window.addEventListener("scroll", requestUpdate, true)
return () => {
window.removeEventListener("scroll", requestUpdate, true)
if (rafRef.current) cancelAnimationFrame(rafRef.current)
}
}, [])The frame callback must directly reset the ref, one scheduler must assign the request, and cleanup must cancel any pending frame. Ref aliases, nonzero initializers, use across effects or handlers, repeated scheduling assignments, and missing cancellation remain unsupported. Static sibling routes still ship zero JavaScript.
Canvas animation migration
A continuous canvas animation can remain ordinary React-shaped source when one effect owns the browser resource. Local effect variables replace component-level mutable refs, while the canvas itself keeps a direct object ref.
const canvasRef = useRef<HTMLCanvasElement>(null)
useEffect(() => {
const canvas = canvasRef.current
const context = canvas?.getContext("2d")
if (!canvas || !context) return
let frame = 0
let visible = true
let x = 20
const tick = () => {
if (visible) context.fillRect(x, 30, 20, 20)
frame = requestAnimationFrame(tick)
}
const observer = new IntersectionObserver(([entry]) => {
visible = entry.isIntersecting
})
const move = () => { x += 8 }
observer.observe(canvas)
window.addEventListener("keydown", move)
frame = requestAnimationFrame(tick)
return () => {
cancelAnimationFrame(frame)
observer.disconnect()
window.removeEventListener("keydown", move)
}
}, [])Kudzu emits one route-specific effect module and preserves native IntersectionObserver, performance, canvas, and frame APIs directly. Component-level mutable value refs, callbacks shared across effects or JSX handlers, uncancelled loops, and missing observer/listener cleanup remain unsupported.
Localized MDX migration
A static locale pair can retain automatic browser-language entry and automatically prefixed links without shipping a package router. Each article route is build-known; only the root selector needs browser JavaScript.
export function getStaticPaths() {
return ["ko", "en"].map(locale => ({
params: { locale }, props: { locale }
}))
}
function LocaleLink({ locale, href, children }) {
const localizedHref = href === "/" ? "/" + locale : "/" + locale + href
return <a href={localizedHref}>{children}</a>
}
// src/pages/index.tsx
useEffect(() => {
const stored = localStorage.getItem("locale")
const locale = stored === "ko" || stored === "en"
? stored
: navigator.languages.some(value => value.startsWith("en")) ? "en" : "ko"
location.replace("/" + locale + location.search + location.hash)
}, [])Build-known MDX emits as static dangerouslySetInnerHTML without eval() or new Function(). Copy blocks use the native clipboard handler and tabs use ordinary state, so interactive MDX components do not require hydration. Request-time Accept-Language negotiation still belongs in CDN or edge redirects when it must happen before HTML.
Browser capability migration
Progressive React UI may keep one direct static navigator capability condition. Kudzu emits the unsupported branch as complete static fallback and checks the immutable browser capability once after mount.
const canShare = "share" in navigator
return <section>
<input value={roomLink} readOnly />
{canShare && <button onClick={async () => {
await navigator.share({ title: "Join my room", url: roomLink })
}}>Share</button>}
<button onClick={async () => {
await navigator.clipboard.writeText(roomLink)
}}>Copy</button>
</section>Supported browsers mount the branch and its handlers through existing conditional ownership. Unsupported browsers receive no Share button or listener in the document or accessibility tree. The capability local must control exactly one direct JSX && branch; aliases, escaped values, dynamic names, composed conditions, ternaries, navigator.canShare(), and arbitrary browser render expressions remain unsupported.
Responsive external-store migration
A reduced Cal.com media-query store can retain the SSR-safe useSyncExternalStore contract. Kudzu turns the false server snapshot into static desktop-first HTML and owns the browser listener through its existing effect lifecycle.
const isMobile = useSyncExternalStore(
callback => {
const media = window.matchMedia("(max-width: 768px)")
media.addEventListener("change", callback)
return () => media.removeEventListener("change", callback)
},
() => window.matchMedia("(max-width: 768px)").matches,
() => false
)
return <main data-layout={isMobile ? "mobile" : "column"}>
{isMobile ? "Mobile booking" : "Desktop booking"}
</main>The query must be one identical static literal in subscribe and snapshot, cleanup must remove the same change listener, and the server snapshot must be false. Parameterized or imported media hooks, dynamic queries, legacy addListener, arbitrary external stores, and non-boolean snapshots remain unsupported. Static sibling routes receive no JavaScript.
Ordinary same-file and relative-imported child components may own local state without specialization into a browser component. A direct JSON-safe primitive parent state passed to a destructured child prop remains reactive in child text, attributes, and effect dependencies. A direct setter or inline/simple const callback may cross one component boundary and two additional direct forwarding components before an intrinsic handler invokes it, supporting value adapters such as event => onValueChange(event.currentTarget.value) without serializing the callback. The first boundary may use an on* prop or pass the direct setter through a set* prop; additional forwarding remains on*-only. Specialized children may own directly serializable useState(), initialize independent state from one direct parent primitive, plain-object, or array-state prop, initialize string state with a direct primitive prop's .toString(), and use useId(), supported effects, and null-initialized object refs. Same-file and relative-imported presentation components also specialize away; nested hooks are supported on unconditional or statically truthy paths. A parent-owned object ref may follow the same proven tree to the intrinsic root. Fourth callback boundaries, additional set* forwarding, aliases, spreads, intermediate adapters, composed prop initializers, and repeated callback uses remain unsupported. Repeated calls receive independent state and effect ownership even when they share generated modules. When a child is inside reactive conditional DOM, removal deletes owned state, drops its handler with the DOM, resolves refs to null, and cleans up effects; re-entry creates fresh state and DOM ownership.
State-backed keyed lists may stay in a same-file or relative-imported component when the page passes its local state identifier directly as a prop. A same-file keyed row may use a direct export function or exported function-valued const and be reused across static and keyed JSX sites. Specialized wrappers and keyed rows accept JSX children plus inline or direct calling-component const object prop spreads, preserving source-order overrides. Missing destructured props may use directly serializable primitive, plain-object, or array literal defaults. One final identifier rest binding may be forwarded exactly once to the direct intrinsic root, where its attributes and events reuse existing analysis. Export-list/default aliases, non-JSX references, dynamic or computed spreads/defaults, indirect rest use, and prototype-sensitive rest properties remain source-diagnosed. Default, named/aliased, and direct named re-export imports are resolved. Kudzu specializes that component to intrinsic list DOM at build time instead of retaining it in the browser.
A direct object-state prop may expose one-segment static fields when its child directly maps an array field. Scalar bindings and that selected array effect dependency reuse the parent signal, Object.is comparison, and binding-backed keyed ownership without field state or a browser component. Dynamic paths, aliases, mutation, child state/ref/ID hooks, and opaque call-site values remain unsupported.
function ItemList({ items }: { items: Item[] }) {
return <ul>{items.map(item => <li key={item.id}>{item.name}</li>)}</ul>
}
return <ItemList items={items} />export function ItemRow({ item }: { item: Item }) {
return <li>{item.name}</li>
}
return <>
<ItemRow item={featured} />
<ul>{items.map(item => <ItemRow key={item.id} item={item} />)}</ul>
</>A keyed row may map multiple direct array properties of its item recursively at any depth. One direct static array field of local object state may root that hierarchy through a supported selector pipeline; immutable whole-object replacement preserves retained rows, latest handlers, and descendant cleanup. A block-bodied keyed map callback may declare one top-level const computed from a direct child collection through the supported pure selector pipeline, then use that alias once as a nested keyed list source before its final JSX return. Nested rows may be intrinsic or recursively specialized through same-file and relative-imported components. Kudzu preserves keyed DOM identity at every level; handlers read the latest item, nested conditions patch bounded DOM, and row key paths own multiple serializable state slots, effects, and null-initialized object refs. Parent capture, multiple or mixed-use child aliases, component cycles, dynamic state, computed object paths, mutation, and callback refs remain unsupported.
CORE
State semantics
Declare local state with the same syntax as React. State may contain serializable primitives, plain objects, or arrays. Setters update logical state immediately and DOM writes batch at the end of the synchronous turn.
const [count, setCount] = useState(0)
function growTwice() {
setCount(count + 1)
setCount(count + 1)
}
const [weather, setWeather] = useState({ temperature: 28, label: "Warm" })
return <p>{weather.temperature}° {weather.label}</p>An anonymous synchronous zero-argument lazy initializer may directly return the same serializable primitive, plain-object, or array literals. Kudzu lowers it at build time; captures and dynamic calls remain unsupported.
Reactive text and attributes may use recursively chained top-level const locals derived from direct state through supported pure primitive expressions. Kudzu substitutes the expression into the binding evaluator and subscribes its source states; arbitrary calls, mutation, cycles, and structural JSX aliases remain unsupported.
Derived text patches a comment-bounded text node without adding a wrapper element. Table cells, options, SVG text, layout, and element selectors retain their authored structure.
Reducers
Reducer dispatch uses the same immediate logical state and batched DOM commit path without retaining a browser component. The optional third initializer may be an inline, same-file, or relative-imported synchronous one-parameter function whose directly serializable result depends only on a directly serializable initial argument.
import todoReducer, { initializeTodos } from "../todoReducer"
const [todos, dispatch] = useReducer(todoReducer, [])
const [prepared, prepare] = useReducer(todoReducer, "Prepared", initializeTodos)
dispatch({ type: "add", title: "Ship" })A dispatch may cross one direct prop boundary into a same-file or relative-imported synchronous component. Kudzu specializes the call so the child handler compiles in the parent reducer scope.
function Controls({ dispatch }: { dispatch: Dispatch<TodoAction> }) {
return <button onClick={() => dispatch({ type: "add", title: "Ship" })}>Add</button>
}
return <Controls dispatch={dispatch} />Relative TypeScript constants and helpers used inside an imported child handler are bundled into the parent handler graph with collision-free local names. The direct child may be a keyed row receiving its item and dispatch; its event handler reads the latest item and it has the same multiple serializable state, effect, condition, and object-ref support as other keyed rows. The key path preserves hooks across updates and reorder and releases them on removal. A specialized dispatch component may also pass one inline or simple const callback containing dispatch into one relative-imported synchronous intrinsic child.
const add = (title: string) => dispatch({ type: "add", title })
return <Input onSubmit={add} />The callback is substituted into the child's compiled event handler; it is not serialized. An inline React useCallback wrapper is erased before specialization. Dispatch and callback components may use destructured string, finite-number, boolean, or null defaults; missing props receive those values during specialization. The current migration form requires direct [state, dispatch] destructuring and a pure synchronous two-parameter reducer imported by default or name from a relative TypeScript module. A direct keyed row has the same multiple serializable state, effect, condition, and object-ref support as other keyed rows. Object, array, computed, and function-call defaults, non-keyed local state inside reducer-dispatch-specialized components, dynamic initializer calls or captures, package, namespace, local, async, and generator reducers, package imports or child imports outside handlers, further forwarding, and reducer dispatch through context remain unsupported.
Zustand stores
Migration source may retain a reduced Zustand store. Kudzu erases the package import, owns the data as one shared-layout state slot, and compiles selected actions into existing functional state updates. React, Zustand, subscriptions, and a generic store runtime are absent from the deploy output.
import { create } from "zustand"
type CartState = {
quantities: Record<string, number>
add: (id: string) => void
}
export const useCart = create<CartState>(set => ({
quantities: {},
add: id => set(state => ({
quantities: {
...state.quantities,
[id]: (state.quantities[id] ?? 0) + 1,
},
})),
}))The shared layout must select the store before route components use it. This gives the store layout lifetime and lets same-group navigation preserve both its value and the layout DOM.
export function ShopLayout({ children }: { children?: unknown }) {
const quantities = useCart(state => state.quantities)
return <>
<header>Cart {quantities.oak ?? 0}</header>
{children}
</>
}
export default function ProductPage() {
const add = useCart(state => state.add)
return <button onClick={() => add("oak")}>Add oak</button>
}Values survive enhanced navigation only within the configured group using that layout. A reload, direct document load, new tab, cross-group link, or native fallback creates a fresh document and restores the store initializer. Use an API or another durable backend when state must outlive the document session.
Context
Context passes default, nested, or reactive object values through component layers without retaining a browser component tree.
type ThemeValue = {
theme: string
setTheme: (theme: string) => void
}
const ThemeContext = createContext<ThemeValue | null>(null)
function Toolbar() {
const value = useContext(ThemeContext)
if (!value) return null
return <button className={value.theme === "dark" ? "theme-dark" : "theme-light"} onClick={() => value.setTheme("light")}>Save</button>
}
function App() {
const [theme, setTheme] = useState("dark")
return <ThemeContext.Provider value={{ theme, setTheme }}>
<Toolbar />
</ThemeContext.Provider>
}Context objects may contain state, setters, nested plain data, and static fields. Consumers may destructure or rename properties. A zero-argument relative custom hook may also directly return useContext(Context) for one local or named relative Context module whose single Provider value is a shorthand object of Provider-owned state/setter pairs and synchronous actions. Direct or nested-handler action calls compile into concrete state operations; action-required state/setter fields that are not publicly selected use compiler-owned collision-free aliases, so same-named consumer locals remain valid. Action functions and the Provider tree do not ship. Dynamic Provider values, multiple Providers, action aliases, and private action captures remain unsupported. Ordinary nested Providers with serializable values remain independent.
Effects
Browser-only work can use an inline useEffect callback with a literal dependency array. Kudzu emits that callback as route-specific ESM and patches only state-bound DOM; it does not ship or rerun the component.
const [items, setItems] = useState([])
useEffect(async () => {
const response = await fetch("/api/items")
setItems(await response.json())
}, [])Effects may update text, attributes, conditions, and keyed lists. They may directly return an inline cleanup function. A document effect cleans up when the document leaves outside the browser back-forward cache; an effect in a conditional branch or supported keyed row mounts and cleans up with that DOM owner.
Use the same ownership for debounced synchronization: create setTimeout() work in a dependency effect and directly return clearTimeout(). Dependency changes and DOM/route removal cancel pending work without a timer runtime.
useEffect(() => {
const timer = setTimeout(() => setSaved(draft), 150)
return () => clearTimeout(timer)
}, [draft])useEffect(() => {
const onResize = () => console.log(window.innerWidth)
window.addEventListener("resize", onResize)
return () => window.removeEventListener("resize", onResize)
}, [])One or more primitive state and runtime parameter identifiers may trigger cleanup and rerun without rerunning the component.
const [event, setEvent] = useState("resize")
const [capture, setCapture] = useState(false)
useEffect(() => {
const listener = () => console.log(event)
window.addEventListener(event, listener, capture)
return () => window.removeEventListener(event, listener, capture)
}, [event, capture])Kudzu coalesces committed dependency changes, awaits every affected cleanup in declaration order, and then runs new setups in declaration order. Dependencies may be direct signal identifiers, aliases holding JSON-safe primitives, destructured ordinary child props passed directly from parent state, direct property paths over ordinary object state, or one top-level immutable local derived through a supported pure primitive expression from state. Derived dependencies subscribe to their source states but compare the calculated value with Object.is; setup and cleanup evaluate the latest expression. Effect setup and directly returned cleanup callbacks may each be one top-level simple const function in the same component; Kudzu substitutes them into the existing handler. A keyed row may also use direct primitive properties such as [item.id] or [version, item.name]; only rows whose selected values changed rerun, reorder does not rerun, and key changes remount. Whole-object, prototype-sensitive, object-valued, spread, arbitrary-call, and dynamic dependencies are rejected. Indirect or dynamically selected callbacks, callback parameters, cleanup parameters or generators, cross-component references, and non-serializable captures remain unsupported. Routes without dependency effects do not load kudzu-deps.js.
A primitive field of a pure synchronous relative calculation, such as [selected.available], may be the sole dependency. The calculation must retain at least two direct state inputs and may also receive static strings, finite numbers other than negative zero, booleans or null. Local const aliases that normalize to those literals are accepted. These arguments add no signals or runtime: the existing evaluator subscribes only to source states and compares the selected result with Object.is. Object/array configuration, mutable locals, composed arguments, undefined, non-finite values and parameter defaults/rest remain unsupported.
An effect may own a relative TypeScript module Worker. Kudzu emits its graph separately and creates it only when the effect mounts.
useEffect(() => {
const worker = new Worker(
new URL("../telemetry.worker.ts", import.meta.url),
{ type: "module" },
)
return () => worker.terminate()
}, [])The supported form requires unshadowed global Worker and URL, exact import.meta.url, a relative .worker.ts literal, and exactly { type: "module" }. Worker imports must use relative TypeScript ESM without JSX, packages, import-equals, dynamic imports, or require(); ordinary runtime imports of Worker source are rejected. Event handlers, imported helpers, and imported keyed-row effects cannot construct relative TypeScript Workers. Give keyed-row Workers to a directly compiled page or local component effect instead.
CORE · NEW
Reactive attributes
State-dependent className, style, disabled, value, and checked use normal React-shaped expressions. Kudzu compiles each expression into an external ESM evaluator and patches only its DOM target.
const [active, setActive] = useState(false)
const [loading, setLoading] = useState(false)
const [name, setName] = useState("Kudzu")
const [subscribed, setSubscribed] = useState(false)
return <>
<div className={active ? "active" : "idle"} />
<div style={{ opacity: active ? 1 : 0, width: active ? 240 : 0 }} />
<button disabled={loading}>Save</button>
<input value={name} onInput={event => setName(event.currentTarget.value)} />
<input type="checkbox" checked={subscribed} onChange={event => setSubscribed(event.currentTarget.checked)} />
<select value={name} onChange={event => setName(event.currentTarget.value)} />
<button aria-expanded={active} data-state={active ? "open" : "closed"} hidden={!active} title={active ? "Active" : "Inactive"} />
</>classNameSets or removes the element's live class attribute.
styleSerializes camelCase object properties, dimensional numbers, unitless values, and CSS custom properties.
disabledToggles the boolean attribute and native disabled property.
valueUpdates the live value property for controlled inputs and selects.
checkedUpdates the live boolean property for controlled checkboxes and radios.
any attributePatches standard, aria-*, and data-* attributes without a compiler allowlist.
Inline SVG
Static and reactive SVG presentation props use familiar React spellings. Kudzu preserves native names such as viewBox and maps aliases such as fillRule, clipRule, strokeWidth, strokeLinecap, strokeLinejoin, opacity/color props, textAnchor, and vectorEffect to SVG attributes. Reactive conditionals and flat intrinsic keyed lists create replacement nodes in the surrounding SVG namespace while preserving keyed identity. For an accessible selected-point tooltip, use ordinary focus, keyboard, and click handlers on keyed SVG points to update parent state rendered in an external HTML element with role="tooltip"; retained handlers read the latest point after updates.
<svg viewBox="0 0 24 24">
{active && <path fillRule="evenodd" strokeWidth={2} strokeLinecap="round" />}
{points.map(point => <circle key={point.id} cx={point.x} cy={point.y} r="2" />)}
</svg>CORE · NEW
Conditional DOM
Child && and ternary expressions compile to bounded DOM ranges. Kudzu inserts or removes only that range and does not rerun a browser component tree.
function MenuBar() {
return <nav><a href="/docs">Docs</a><a href="/about">About</a></nav>
}
const [open, setOpen] = useState(false)
return <>
{open
? <button onClick={() => setOpen(false)}>Close menu</button>
: <button onClick={() => setOpen(true)}>Open menu</button>}
{open && <MenuBar />}
</>Menu mountedMenu dormantMenu dormant
Top-level and block-scoped immutable const values can hold static JSX, state-dependent branches, aliases, and nested JSX locals. Kudzu compiles their initializers to the same bounded ranges as inline conditions.
const menu = open ? <MenuBar /> : <p>Menu dormant</p>
const content = open && menu
return <main>{content}</main>if (loading) return <Loading />
if (failed) return <ErrorView />
return <Content />
let view
if (open) view = <MenuBar />
else view = <p>Menu dormant</p>
return viewTerminal early-return chains and one adjacent exhaustive let assignment normalize to the same bounded conditional ranges. Branches may contain only that return or assignment; effectful statements and non-exhaustive or later assignments are rejected for reactive rendering.
A historical local 1,000-component A/B snapshot emitted identical runtime assets and condition counts. Mixed block-local, early-return, and assignment source added 18 B gzip for evaluator exports and increased clean build time from 590 ms to 604 ms. That benchmark workspace and its raw arrays are not tracked in this repository.
Both branches are rendered into inert templates at build time. Conditional rendering is a UI mechanism, not an authorization boundary: do not place secrets or access-controlled content in a dormant branch, and avoid build-time side effects in branch components.
CORE · NEW
Keyed lists
Map local array state directly to one keyed intrinsic element. Initial items are static HTML; browser updates add, remove, update, and move existing keyed elements without a VDOM or remount.
type Tree = { id: number; name: string }
const [items, setItems] = useState<Tree[]>([
{ id: 1, name: "Oak" },
{ id: 2, name: "Pine" }
])
function create() {
setItems([{ id: 1, name: "Oak" }, { id: 2, name: "Pine" }])
}
function add() {
const id = Math.max(0, ...items.map(item => item.id)) + 1
setItems([...items, { id, name: "Vine " + id }])
}
function reverse() {
setItems([...items].reverse())
}
const rows = items.map(item => <li key={item.id} style={{ opacity: item.id % 2 ? 1 : 0.7 }}>
<span>{item.name}</span>
{item.id % 2 ? <strong>Odd</strong> : <small>Even</small>}
<button onClick={() => setItems(items.filter(entry => entry.id !== item.id))}>Remove</button>
</li>)
return <><button onClick={create}>Create</button><button onClick={add}>Add</button><button onClick={reverse}>Reverse</button><ul>{rows}</ul></>A top-level same-file row component or one imported from a relative TypeScript module can be the map root. Default, named, aliased, and named re-export imports resolve at build time. Kudzu specializes each call, supporting projected props, callback props, simple local calculations, and inline effects without shipping a browser component runtime.
function TreeRow({ name, onRemove }: { name: string; onRemove: () => void }) {
const label = name.toUpperCase()
return <li>{label}<button onClick={() => onRemove()}>Remove</button></li>
}
const rows = items.map(item =>
<TreeRow key={item.id} name={item.name} onRemove={() => setItems(items.filter(entry => entry.id !== item.id))} />
)Nested child components
One child map may read a direct array property from its parent item. Its row may be intrinsic, declared in the same file, or imported from a relative TypeScript module. The component is specialized away at build time; existing parent and child keys retain DOM identity through updates and reorder.
function ItemCard({ item, onSelect }: {
item: Item
onSelect: () => void
}) {
return <li>
<span>{item.title}</span>
{item.available
? <strong>Available</strong>
: <small>Unavailable</small>}
<button onClick={() => onSelect()}>Select</button>
</li>
}
return <main>{categories.map(category =>
<section key={category.id}>
<h2>{category.title}</h2>
<ul>{category.items.map(item =>
<ItemCard
key={item.id}
item={item}
onSelect={() => setSelected(item)}
/>
)}</ul>
</section>
)}</main>parent.<field>. Multiple sibling maps are supported recursively without a numeric depth limit. Nested handlers read their latest item.The whole map may also live in a same-file or relative-imported wrapper receiving items directly from local state. Default, named/aliased, and direct named re-export wrappers are specialized to intrinsic DOM; package, namespace, star-export, derived-prop, and effectful wrappers remain unsupported.
- OddEven
# - OakOdd
#1 - PineEven
#2
Each item and nested object must be an ordinary JSON-safe plain object with a unique string or finite-number key. Direct item reads use compact markers; pure synchronous derived text, attributes, object styles, and nested item conditions compile to external evaluators. A flat row text or attribute expression may combine its item or index with direct primitive parent state, supporting selected-row classes and ARIA attributes while retaining keyed DOM identity. An expression-bodied callback with one item parameter may also return condition && <Row /> or condition ? <Row /> : null; Kudzu lowers the pure item/state condition to its existing filter path so removal cleans up row ownership and re-entry starts fresh. Immutable local aliases may compose inline pure filter, direct-property flatMap, Array.from, immutable slice(start, end?), and toSorted((left, right) => expression) and feed multiple keyed list sites when every alias reference is an analyzable collection source. Filter predicates may combine pure string methods and direct primitive state for reactive search; slice bounds may use numeric literals or supported state expressions for pagination. Sorting evaluates a constrained two-parameter comparator over a copy and rejects mutating sort(). A relative-imported named or default synchronous one-parameter function may directly return the same pure pipeline; Kudzu composes its selector without executing or shipping the function. Ordinary map callbacks may use (item, index), and key={index} intentionally owns DOM by position; conditional-root maps reject indexes because filtering changes their meaning.
Any specialized keyed row may own multiple directly serializable state values, effects with supported primitive dependencies, and object refs initialized with null. Its structural site and ancestor key path preserve ownership through updates/reorder; removal cleans up and a re-add starts fresh.
Specialized rows accept direct same-file exports reused across static and keyed JSX sites, JSX children, prop spreads from inline objects or one direct const object resolved through calling-component scopes, directly serializable eager or lazy state literals, directly serializable literal defaults, and one final rest binding forwarded exactly once to the direct intrinsic root. A block-bodied keyed map callback may also compute one direct child collection const and use it once as a nested keyed list source. Package or namespace row imports, export-list/default aliases, non-JSX component references, dynamic/computed prop spreads or defaults, indirect/repeated rest use, rest-forwarded children, multiple or mixed-use child collection locals, parent capture, component cycles, fragments, dynamic state initializers, callback refs, mutation, arbitrary collection callbacks, and dangerouslySetInnerHTML remain unsupported. Put keyed rows inside an explicit tbody, thead, or tfoot.
CORE
Event handlers
Setter-only handlers compile to ordered commands. Conditions, browser APIs, event reads, and async code compile to external ESM without eval.
Inline or named handlers may use browser globals such as localStorage, FileReader, and alert; Kudzu leaves them in browser ESM instead of serializing them as build-time captures.
Native handlers use direct DOM listeners with normal currentTarget, bubbling, default-action, and propagation semantics. Handler modules load before listener registration, so preventDefault(), stopPropagation(), and stopImmediatePropagation() work synchronously as expected.
When keyed or conditional removal, enhanced navigation, or document disposal unmounts a native handler's DOM owner, pending async work loses state-write, queued-commit, and captured-ref authority. Late setters and commits become no-ops, and refs resolve to null instead of a replacement node that reused the same ownership ID. Promises and ordinary reads may finish; Kudzu adds no scheduler.
Native handlers may call default, named, or namespace helpers from relative TypeScript modules. Kudzu bundles the reachable helper graph into handler ESM and shared chunks. Helper runtime imports must remain relative; package imports inside helper graphs, dynamic imports, JSX helpers, and direct imported event callbacks are rejected. A package import is supported when referenced directly inside an intrinsic JSX event callback or inline owned effect setup and cleanup callback.
Direct async handlers and directly returned relative custom-hook callbacks may use native navigator.clipboard.writeText(). Keep permission failure handling and user feedback in application state; Kudzu emits no clipboard API or runtime of its own.
Object refs resolve DOM elements while their native handler owner is mounted. They initialize with null, resolve to null after ownership release, and need no effect or hydration lifecycle.
const inputRef = useRef<HTMLInputElement>(null)
return <><input ref={inputRef} /><button onClick={() => inputRef.current?.focus()}>Focus</button></>async function loadStatus() {
setLoading(true)
const response = await fetch("/api/status")
setLoading(false)
}import { normalizeStatus } from "../lib/status"
async function loadStatus() {
const response = await fetch("/api/status")
setStatus(normalizeStatus(await response.json()))
}const copy = async () => {
try {
await navigator.clipboard.writeText(value)
setStatus("copied")
} catch {
setStatus("copy failed")
}
}CORE
Client captures
Handlers and reactive expressions may capture serializable component locals and destructured props.
Strings, booleans, numbers, arrays, plain objects, and destructured props.
Function captures, symbols, bigints, cycles, and class instances. Relative imported helpers use browser ESM instead of capture serialization.
REFERENCE
Compiler architecture
Kudzu treats supported React-shaped TypeScript and TSX as compiler input. It does not retain component functions or reproduce React rendering in the browser.
React-shaped TypeScript/TSX
-> ordered normalization passes
-> state/effect/handler/binding/list analysis
-> complete HTML + route-specific capability ESMFocused passes lower supported React, Router, browser-signal, resource-lifecycle, and render-control shapes. Parent pointers are repaired after structural changes.
The no-write source compiler finalizes JSON-safe ModuleIR and rejects unsupported syntax with source locations. Build-time rendering emits RouteIR, then pure capability planning projects CapabilityIR.
Dedicated codegen modules produce native handlers, reactive evaluators, keyed-list expressions, effects, parameters, and navigation capabilities.
Final route plans remove unreferenced handlers and Workers. Static routes emit complete HTML and no browser JavaScript.
The compiler uses TypeScript AST transforms through transpileModule(); semantic type checking remains the separate tsc --noEmit project check. Each build owns an explicit ProjectSession containing its root, source records, graph operations, compiler paths, Worker compiler, canonical parsed modules, stable ModuleSymbols, and source-local SiteIds. Read-only consumers reuse canonical facts while each transformer context receives a private clone. Browser runtime is capability-specific rather than absent: a route receives only the modules it needs. The architecture packet records the current compiler and ordered large-application program.
REFERENCE
Build output
npm run build
dist/
├── index.html
├── docs/index.html
└── assets/
├── style.css
├── kudzu.js
├── kudzu-deps.js (when dependency-only effects are used)
├── kudzu-navigation.js (legacy navigation.routes)
├── kudzu-navigation-<route-hash>.js (per navigation group)
├── kudzu-binding.js (when used)
├── kudzu-effect.js (when effects are used)
├── kudzu-style.js (when reactive styles are used)
├── effects/ (when route-specific effects are used)
├── params/ (when runtimeParams is used)
├── handlers/ (evaluators and imported helpers)
├── kudzu-list.js (when used)
├── kudzu-native.js (when used)
└── kudzu-serialization.js (when used)Static pages ship no JavaScript. Interactive pages receive only the command runtime and external handler or binding modules they use. Equal standalone capability signatures reuse one emitted runtime family; enhanced-navigation groups use one union family to preserve layout state and lifecycle singleton identity. Runtime bracket pages emit one fallback HTML file plus a route-specific parameter matcher. Production artifacts and afterBuild() output complete in a staging tree; public files cannot replace generated paths, and the previous dist remains available after ordinary build or hook failure. Ordered host rewrites are written to .kudzu/kudzu-plan.json. Exact route signatures, assigned runtime families, and emitted handler, Worker, stylesheet, and transitive chunk edges are written to .kudzu/kudzu-artifacts.json and passed to afterBuild().
Verify the application
Normal builds scan generated route HTML after afterBuild, reporting counts with and without script/modulepreload text markers, unreadable files, and up to five bounded paths per category. Reads run in batches of 64. This lexical inventory is not proof of zero JavaScript: comments and inert content can match, inline event attributes are not checked, and extra public HTML is outside the route inventory. Failed, quiet, and JSON builds omit the summary.
A successful build validates supported source and emits outputs, not browser behavior or accessibility. Verify visible DOM, interactions, labels, status updates, keyboard focus, and responsive layouts as needed. Binding comments and inert templates make raw HTML string matching or tag stripping unreliable for rendered text. Use bounded checks with concise counts and failing paths; missing tools or truncated responses are not passes. Rerun affected checks after source changes.
For optional compiler/artifact investigation, kudzu inspect --json reports reachable source, routes, capabilities, and blockers; kudzu explain --route /exact-path --json reports one exact emitted route, including any configured base prefix, and its artifact reasons and bytes. Both commands rebuild the project, including build hooks and output emission; neither merely reads the previous build. These reports cover compiler and artifact facts, not full browser or accessibility proof. Use them when those facts are needed, not as a mandatory sequence after every successful build.
REFERENCE
Benchmarks
1.0.0 remains blocked. See the release notes and performance records for the separate evidence.Project application
| Strategy | Load / range median | Median DOM nodes |
|---|---|---|
| Direct 10,000 rows | 138.2 ms / n/a | 90,023 |
| Pagination, 100 rows | 39.6 / 5.8 ms | 2,867 |
| Scroll window, 100 rows | 40.7 / 5.8 ms | 2,868 |
Pagination is selected: it avoids direct DOM scale, has the lower bounded-range median and heap, and preserves native page, keyboard, focus, and variable-height behavior without a scroll listener. No semantic primitive, compiler pass, public API, or runtime concept was added.
Public commerce comparison
| Target | Initial JS gzip | Reliable click | Resilience |
|---|---|---|---|
| Kudzu 0.16.8 | 4.2-9.9 KiB | 300 ms | 15 / 18 |
| Astro 7.1.3 + React | 60.6-61.1 KiB | 1,500 ms | 12 / 18 |
| React Router 8.3.0 | 103.8-104.6 KiB | 2,000 ms | 8 / 18 |
| TanStack Start 1.168.32 | 103.5-104.1 KiB | 2,000 ms | 9 / 18 |
| Next.js 16.2.11 | 144.2-146.2 KiB | 3,000 ms | 8 / 18 |
The public 1,000-product fixture measures six routes. Initial JavaScript is what Chrome received; browser results use seven fresh sessions at 4x CPU slowdown and Slow 4G. Reliable click is the first tested delay after first paint with zero lost add-to-cart attempts. Resilience counts six matched capabilities under blocked, delayed, and partially missing JavaScript.
Tracked Worker fixture
The current loaded-host Worker build median is 696.6 ms without a timing comparison. Its Worker graph is 907 B raw / 477 B gzip and its aggregate window graph is 14,456 B raw / 6,160 B gzip. The focused Chrome test verifies throughput, cadence, bounded history, stale writes, and 30-cycle termination and listener ownership.
The full performance record separates current maintained runs from historical and external-workspace evidence.
REFERENCE
Current limits
- A direct object-state prop may expose one-segment static fields when its child directly maps an array field. Scalar bindings and that selected array effect dependency reuse the parent signal and existing ownership. Dynamic paths, aliases, mutation, child state/ref/ID hooks, and opaque call-site values remain unsupported.
- Repeated ordinary same-file and relative-imported child components own independent state IDs and effect records. Direct JSON-safe primitive parent state passed to a destructured child prop remains reactive in child DOM bindings and effect dependencies. A direct setter or inline/simple
constcallback may cross one boundary and two additional direct forwarding components before one intrinsic handler invokes it. The first boundary may use anon*prop or one directset*setter prop; additional forwarding remainson*-only. Specialized children may own directly serializableuseState(), initialize independent state from one direct primitive, plain-object, or array-state prop, use direct primitive prop.toString()string-state initialization, and ownuseId(), supported effects, andnull-initialized object refs. Nested presentation components recursively specialize, including hooks on unconditional or statically truthy paths, and a parent-owned object ref may follow the same proven tree. Reactive conditional branches drop handlers and refs with removed DOM, delete child state, and clean up effects, then recreate ownership on re-entry. Fourth callback boundaries, additionalset*forwarding, intermediate adapters, aliases, spreads, repeated callback uses, dynamic nested hook paths, composed state initializers, derived props, arbitrary callbacks, callback refs, captures, and dynamic calls remain unsupported. - Reactive JSX text and attributes may reference recursively chained top-level immutable locals derived through supported pure primitive expressions from direct state. Kudzu substitutes the expressions and subscribes each source state. Fixed-locale
new Intl.NumberFormat("literal").format(Math.round(expression))display chains are supported; dynamic locales/options, arbitrary calls, mutation, cycles, shadowed or block-local declarations, and structural JSX locals remain unsupported. forwardRef()must directly initialize one top-levelconstcomponent with one inline synchronous(props, ref)render function. The object ref must appear exactly once as the direct intrinsic root'sref. Indirect render functions, callback/composed refs, fragments, component roots, nested or repeated forwarding, imperative handles, andmemo(forwardRef(...))are not supported.useId()must initialize one top-levelconstidentifier and accepts no arguments. Values are deterministic within the rendered document and add no browser JavaScript. Keyed rows may use the value directly in intrinsicid,htmlFor, static ARIA IDREF,form,headers,list, andpopovertargetattributes; cloned rows scope it by key path. Aliases, text, handlers, arbitrary attributes, and component-prop forwarding remain unsupported.- Object refs initialize directly with
null, including refs declared by keyed row components. React migration input may instead use one directcreateRef()attached exactly once to an intrinsic element when that ref enters the supported outside-click hook shape. Callback refs, general mutable value refs, non-nullinitializers, aliases, and reactivedangerouslySetInnerHTMLare not supported. The single private timeout ref described below is a narrow compiler-owned exception. useReducerrequires a pure synchronous two-parameter default or named reducer imported from a relative TypeScript module. An optional inline, same-file, or relative-imported synchronous one-parameter initializer may derive a directly serializable literal only from a directly serializable initial argument. Dispatch may cross one direct prop boundary into a specialized same-file or relative-imported synchronous component, including a keyed row with latest-item handlers and the ordinary keyed-row state/effect/ref capabilities. That component may pass one dispatch-containing callback, optionally wrapped in ReactuseCallback, into one relative-imported synchronous intrinsic child. These specializations accept directly serializable primitive, plain-object, or array literal defaults and one final rest binding forwarded exactly once to a direct intrinsic root. Computed defaults, indirect rest use, non-keyed local state inside reducer-dispatch-specialized components, dynamic initializer calls or captures, package, namespace, local, async, and generator reducers, package imports or child imports outside handlers, further forwarding, and dispatch through context are not supported.- Reduced Zustand migration stores require one exported
create(set => ({ data, ...actions }))store with one serializable data property, direct property selectors, and synchronous capture-free merge-form actions. A shared layout must initialize the store outside keyed rows. Derived selectors, multiple data properties, middleware,get, subscriptions, equality functions, persist/devtools wrappers, async actions, helper captures, replacement updates, and indirect action forwarding are not supported. - Reactive conditionals and flat intrinsic keyed lists are supported inside SVG and preserve the SVG namespace for inserted nodes. Keyed SVG points may use focus, keyboard, and click handlers to update parent state for an external accessible HTML tooltip; retained handlers read the latest item after list updates. Keyed-item conditions, nested SVG keyed lists, reactive MathML structures, and namespaced SVG attributes such as
xlinkHrefare not yet supported. - Keyed lists require a local array-state anchor and intrinsic roots or specialized same-file/relative row components. Directly exported same-file rows may be reused at static and keyed JSX sites. Immutable local collection aliases may compose inline pure
filter, direct-propertyflatMap,Array.from, immutableslice(start, end?), and expression-bodiedtoSortedselectors and feed multiple keyed list sites when every reference is an analyzable collection source. Direct primitive state may drive pure string search predicates, slice bounds, comparator expressions, and pure flat-row text or attribute expressions that also read the current item/index. One-parameter expression callbacks may returncondition && <Row />orcondition ? <Row /> : null; the condition may read the item and direct primitive parent state, while nested forms are item-only. Object/array state, nested-row parent state, conditional map indexes, alternate JSX fallbacks, and block-bodied conditional maps remain unsupported. A relative-imported named or default synchronous function may accept one supported collection and directly return one such pipeline. Captures, additional statements, arbitrary bounds or comparators, package/namespace transforms, mutatingsort(), and async functions remain unsupported. Ordinary map callbacks accept(item)or(item, index); field keys own item identity and index keys own positional identity. - A keyed row may contain multiple sibling maps over direct parent-item array properties recursively without a numeric depth limit. One direct static array field of local object state may root the hierarchy through a supported selector pipeline while immutable whole-object replacement preserves retained rows and descendant cleanup. A block-bodied keyed
mapcallback may declare one top-level direct-child collectionconstthrough a supported pure selector and use it once as a nested keyed list source before its final JSX return. Intrinsic, same-file, directly exported same-file, or relative-imported rows support recursive specialization, analyzable prop spreads, directly serializable literal prop defaults, direct intrinsic rest forwarding, forwarded JSX children, nested conditions, latest-item handlers, multiple directly serializable state slots, supported effects, andnull-initialized object refs. - Multiple or mixed-use computed child collection locals, parent capture in child rows, component cycles, package or namespace row imports, export-list/default aliases, non-JSX component references, dynamic/computed prop spreads or defaults, indirect/repeated rest use, rest-forwarded children, derived-expression captures, dynamic row state, non-
nullrefs, and arbitrary or asynchronous collection callbacks remain unsupported. - Reactive statement control flow supports terminal returns and adjacent exhaustive JSX assignment; effectful branches, loops,
switch,try, and later reassignment remain unsupported. useEffectsupports inline block-bodied callbacks, one same-component top-level simpleconstsetup function, directly returned inline or simpleconstcleanup, DOM ownership in ordinary conditional children and supported keyed rows, and literal dependency arrays. Arrays may contain multiple direct JSON-safe primitive state/runtime parameters, destructured child props passed directly from parent state, or direct property paths over ordinary object state; every selected value is compared withObject.is, and same-turn changes cause one cleanup and rerun. One top-level immutable local derived through a supported pure primitive expression from state may also be a dependency, with latest setup/cleanup evaluation. Keyed rows accept directitem.<field>primitive properties. An inline canvas effect may own local mutable drawing state, a recursive animation frame, oneIntersectionObserver, and native listeners when cleanup cancels, disconnects, and removes those resources. Component-level mutable refs or callbacks shared across effects/handlers, whole-object, prototype-sensitive, object-valued, spread, arbitrary-call, dynamic or indirect callbacks, callback/cleanup parameters or generators, cross-component functions, and other return forms are not supported.- Direct async handlers and directly returned relative custom-hook callbacks may call native
navigator.clipboard.writeText(); applications own permission errors and feedback state. Effect-owned debounce usessetTimeout()plus directly returnedclearTimeout()cleanup. A relativeuseDebounce(value, delay)hook may accept one direct primitive state and numeric literal delay, initialize and directly return one debounced state, and own one exact timeout dependency effect with cleanup. A relative outside-click hook may receive one direct intrinsic DOM ref and inline literal setter callback when it owns one static documentmousedownlistener, testsref.current.contains(event.target), and removes the same listener in cleanup. One directly returned relative custom-hook callback may own oneuseRef<number | null>(null), directly clear its previous timer, and assign one numeric-literal-delay timeout when one empty-dependency effect directly clears the latest timer on cleanup. Multiple refs/listeners, dynamic events or delays, non-primitive debounce inputs, aliases, intervals, unowned delayed writes, and arbitrary timed callback graphs remain unsupported. - An ordinary component may declare top-level
useRef(null)anduseRef(0)values when every direct.currentreference belongs to one inline effect setup, its nested callbacks, and cleanup. Kudzu moves them into an invocation-private effect closure for SDK handles, WebSockets, generation tokens, and animation-frame handles. JSX DOM refs remain component-owned; aliases, nonzero initializers, cross-effect/event use, and escaped ref objects are rejected. - Relative TypeScript Workers are limited to unshadowed
new Worker(new URL("../name.worker.ts", import.meta.url), { type: "module" })directly inside an inline effect. Worker graphs allow relative TypeScript ESM runtime imports only; JSX, package runtime imports, import-equals declarations, dynamic imports,require(), outside-source paths, ordinary runtime imports of Worker source, event handlers, imported helpers, and imported keyed-row effects are rejected. - React Router migration input supports a named or aliased
Linkused directly in JSX with exactly one static root-relativetoand native anchor props. It supports a direct top-leveluseMatch("/exact-path")route binding folded case-insensitively from the build-known route, a direct zero-argumentuseParams()call on aruntimeParamsbracket route, one top-levelconst [params]orconst [params, setParams]useSearchParams()binding with direct staticget()locals, the exact numeric pagination fallbackNumber(params.get("page")) || 1, a string fallback from a static element of a named relative immutable array, and synchronous inline setter updaters, and one top-leveluseNavigate()binding called directly from nested browser callbacks with safe static root-relative destinations. Kudzu appliesbase, redirects path/query reads to route-specific capabilities, lowers search writes to native push history or exactly{ replace: true }to replace history, lowers navigation tolocation.assign()orlocation.replace(), and erases the package import. Layout/runtime-parameter match use, dynamic match patterns or destinations, match params/wildcards/query/hash/trailing slashes,NavLink, direct-value query setters, dynamic outer query methods or fallback indexes, aliases, broader query wrappers, render-time writes/navigation, other options, other router hooks, router-only props, spreads, conflictinghref, and default/namespace imports are not supported. Build-knowngetStaticPaths()routes use props. - Named or aliased React Bootstrap
RowandColmay render directly with children, one staticclassName, and numeric literalColspans from 1 through 12 forxs,sm,md,lg,xl, andxxl. They lower to native Bootstrap grid classes while route-reachable CSS remains application-owned. Boolean/object/dynamic grid props, offsets/orders, dynamic classes, spreads, default/subpath/namespace imports, non-JSX references, and other React Bootstrap components are not supported. runtimeParams = truerequires full bracket segments, cannot be combined withgetStaticPaths(), and requires an exact-file-first fallback rewrite on the production host. Catch-all runtime parameters are not supported.- Static locale routing uses
getStaticPaths(), build-known locale props, relative components that emit prefixed native anchors, and optionally one root mount effect reading a stored locale ornavigator.languages. Runtime-resourcereact-i18nextuseTranslation()calls receive a migration diagnostic rather than silently selecting one language or retaining i18next. Kudzu does not executenext-intlor provide request-timeAccept-Languagenegotiation; configure that redirect at the host or edge when browser-side selection is too late. - There is no request-time SSR, server actions, default/general router, HMR, or DevTools. Opt-in navigation supports independent emitted exact/runtime groups with one shared layout per group; cross-group path domains cannot overlap. Conditional and keyed effects follow their layout or route lifetime. Nested retained-layout owners remain unsupported until three unrelated executable application routes prove an intermediate lifetime that static composition cannot express.
- Imported client helpers require relative TypeScript modules; package runtime imports, dynamic imports, JSX helpers, and non-serializable captures are rejected.
- Compilation follows relative runtime imports and re-exports from page entries, plus validated Worker references. Unreachable TypeScript files are excluded. A direct map over an imported exported JSON-safe immutable array folds to static rows. A synchronous relative calculation may initialize one top-level immutable result whose direct static primitive fields feed reactive JSX; state arguments use build values initially and route binding ESM reevaluates the helper after commits. A selected primitive field may be the sole effect dependency with at least two direct state inputs plus static string, finite-number (not negative zero), boolean or null arguments and const aliases normalized to those literals. Object/array configuration, mutable or composed arguments, undefined/non-finite values and parameter defaults/rest remain unsupported. One direct array field may feed a keyed intrinsic map through a compiler-owned array anchor, preserving retained DOM and SVG keys. The field must remain a JSON-safe array; indirect result aliases, dynamic fields, and nested calculated SVG lists are unsupported.
- Package imports may be referenced directly inside intrinsic JSX event-handler callbacks or inline owned effect setup and cleanup callbacks. One guarded literal package
import()may defer an owned effect graph and share it across route owners through native ESM. Render-time, helper-indirect, mixed, side-effect, unguarded, and shadowed-hook package imports are rejected.