A few weeks ago, I wrote about Kudzu, an open-source compiler-first web framework that lets you write familiar React-shaped TypeScript/TSX without shipping React, a VDOM, or hydration by default.
The original idea is straightforward:
- write familiar TSX
- analyze components, state, events, lists, effects, and dependencies at build time
- emit complete HTML
- ship only the JavaScript capabilities a page actually needs
That part works.
But I think the more interesting problem starts after that.
Static pages were never the hard part
A compiler can make a static page extremely small.
That does not automatically make it a serious application framework.
The real question is:
Can Kudzu grow into something capable of building large, long-lived applications without eventually recreating the same generic runtime model it was designed to remove?
By “real applications,” I mean things like:
- shared application state
- async/server data
- loading and error states
- cache invalidation
- optimistic mutations
- complex forms
- nested routing
- authentication and permissions
- realtime updates
- large lists and virtualization
- editors
- charts and maps
- code splitting
- long-lived application state
If Kudzu cannot handle those well, then it risks ending up as a very good static-site compiler with some interactivity.
That is not the goal.
The goal is not zero JavaScript
This distinction matters.
I do not think a serious web application should avoid client-side JavaScript at all costs.
A dashboard may genuinely need:
- shared state
- navigation
- server data
- caching
A chat application may genuinely need:
- WebSocket connections
- keyed lists
- lifecycle management
- optimistic updates
An editor may need a substantial JavaScript library.
That is fine.
The goal is not:
zero JavaScript
The goal is closer to:
zero JavaScript that exists only because the framework needs it
A static route might ship almost nothing.
A complex application route might ship much more.
But that code should correspond to capabilities the application actually uses.
Where I do not want Kudzu to go
There are two easy ways to make compatibility grow quickly.
I think both are dangerous.
1. Rebuild React underneath Kudzu
The simplest escape hatch would be to add:
- a generic component tree
- VDOM reconciliation
- hydration
- a generic rerender loop
- a React-compatible fallback runtime
That would make more React code easier to support.
But it would also erase much of the reason Kudzu exists.
The architecture I want to preserve is closer to:
state change
↓
known dependency
↓
specific DOM / resource / list update
instead of:
state change
↓
component rerender
↓
virtual tree
↓
diff
↓
DOM update
2. Add endless compiler special cases
The other failure mode is less obvious.
It looks like this:
React Router special case
→ Zustand special case
→ TanStack Query special case
→ React Hook Form special case
→ MUI special case
→ browser API special case
→ ...
That may work for a while.
But eventually the compiler becomes a catalog of package-specific syntax patterns.
Then users are no longer writing normal React-shaped code.
They are writing:
“the specific shape Kudzu happens to understand”
That is also a problem, especially for AI-assisted development.
If an AI agent constantly has to read compiler limitations, rewrite source into special forms, retry builds, and search framework docs, the token savings disappear.
The direction I am exploring
The architecture I am currently leaning toward has three layers.
React / npm ecosystem source
↓
compatibility / normalization
↓
small Kudzu application model
↓
compiler
↓
HTML + only required runtime capabilities
The important part is the middle.
Kudzu needs a small set of application capabilities that are powerful enough to build serious applications.
Not hundreds of framework-specific features.
Something closer to:
- local state
- shared state
- derived values
- events
- async resources
- mutations
- navigation
- keyed/owned DOM ranges
- lifecycle
- external UI instances
- lazy capabilities
The exact list is still something I am working through.
The goal is that a small number of primitives can be composed into many different applications.
What about third-party libraries?
I do not think Kudzu should try to compile the internals of every library.
Some packages should simply run normally.
For example:
- Zod
- date-fns
- Dexie
- Three.js
- MapLibre
If a library already owns a canvas, WebGL context, or DOM subtree, Kudzu may only need to manage:
mount
update
dispose
lifetime
For example:
Kudzu
└─ map container
└─ MapLibre owns the internals
No VDOM is required for that.
The same idea may work for:
- CodeMirror
- Monaco
- ProseMirror
- Chart.js
- ECharts
- data grids
- media players
This is very different from embedding a React island.
Kudzu would not try to recreate the package's internal rendering model.
It would only own the boundary.
React ecosystem compatibility still matters
I do want practical React migration compatibility.
But I think there is an important distinction between:
React runtime compatibility
and:
React source compatibility
The long-term goal is closer to:
maximize React ecosystem source compatibility while minimizing React runtime compatibility
That means different packages may need different strategies.
Native
Normal JavaScript/TypeScript packages can run as-is.
Compiled
Some familiar React-shaped patterns can be understood and compiled away.
Normalized
Some source can be transformed into simpler HTML/JavaScript/Web API code before it reaches the core compiler.
For example:
<Link to="/users">Users</Link>
could conceptually become:
<a href="/users">Users</a>
Adapter
Some libraries may need package-specific migration logic.
External owned UI
Some complex UI libraries can own a bounded DOM/canvas area.
Partial or unsupported
Some React libraries may rely deeply on React reconciliation or component-tree semantics.
I think it is better to explicitly say “unsupported” than silently fall back to a React runtime.
Performance so far
There are two sets of performance evidence I have been using.
The public cross-framework benchmark currently uses an earlier Kudzu version, so I do not treat it as a current-version benchmark.
Still, it is useful for understanding the architecture.
On a commerce fixture under Slow 4G + 4× CPU throttling, the published results included:
- Kudzu first interactive listing: 450ms
- Astro: 2.22s
- TanStack Start: 2.90s
- React Router: 2.96s
- Next.js App Router: 3.54s
Kudzu did not win everything.
The 3-step form benchmark exposed a clear weakness in cross-page state propagation:
- TanStack: 191ms
- Astro: 196ms
- React Router: 395ms
- Next.js: 365ms
- Kudzu: 700ms
I think that loss is useful.
It shows exactly the kind of application-level problem that matters more than shaving another few hundred bytes off a bundle.
Kudzu's own maintained performance evidence also showed a stateful capability bundle substantially smaller than React/Vue/Svelte in that specific fixture, and large generated-route builds scaling competitively into the thousands of routes.
I do not consider any of those numbers universal proof.
They are fixture-specific.
The part I care about most now is whether those characteristics survive as application complexity increases.
The next test should not be another blog
I think the next serious test for Kudzu needs to be an actual application.
Something like a small Linear / GitHub Issues / Jira-style app with:
- login
- workspaces
- shared state
- nested routing
- server data
- loading/error states
- caching
- mutations
- optimistic updates
- forms
- modals
- search/filtering
- realtime updates
- a large list
- lazy-loaded editor or chart
The goal is not to build another demo that happens to match the compiler.
The goal is to let the application expose where the compiler model is insufficient.
Then:
application fails
↓
identify the general missing capability
↓
check whether existing primitives can express it
↓
only add a new primitive if the limitation repeats across multiple real apps
That is the development loop I want to follow.
AI development is part of the design
There is another reason I care about keeping the application model small.
AI coding agents already understand React-shaped TypeScript/TSX extremely well.
I do not want Kudzu to replace that with an entirely new DSL or dozens of Kudzu-specific APIs.
Ideally an AI should be able to write familiar code while the compiler handles things the model should not need to reason about repeatedly.
That includes things like:
- dependency tracking
- unnecessary rerender semantics
- framework boilerplate
- some migration transformations
- precise diagnostics
The metric I eventually want to measure is not just bundle size.
It is:
cost per successful task
For the same feature, using the same coding agent:
- how many tokens were used?
- how many files were opened?
- how many build attempts?
- how many compiler failures?
- how many edits?
- how much framework documentation had to be read?
- did the final application actually behave the same?
If Kudzu uses less client JavaScript but makes the AI fight the compiler for twice as long, that is not a win.
Where Kudzu is now
Kudzu is still experimental.
The current work is moving increasingly toward application-level behavior rather than only static output.
But I do not think the large-application question is solved yet.
That is the part I am trying to figure out now.
The two constraints I am trying to hold at the same time are:
1. Do not rebuild React underneath the compiler.
2. Do not let “minimal runtime” prevent Kudzu from becoming a real application framework.
I think the interesting architecture is somewhere between those two extremes.
Project:
GitHub:
https://github.com/kudzujs/kudzu
Public benchmark:
https://github.com/SimYunSup/kudzu-based-bench
I'd be interested in hearing from people who have worked on large React, Vue, Svelte, Solid, or similar applications:
What browser-side capabilities do you consider genuinely unavoidable for serious web applications?
And which ones do you think mostly exist because of the rendering model of the framework you are using?
Top comments (0)