Vapor Mode: Real Local Updates And Real Functional Programming
Vapor Mode is where Vue JSX stops creating a Virtual DOM tree for the hot path. The compiler turns JSX into a DOM block factory plus small reactive operations. The component still reads like a function, but the updates are no longer component-sized render passes.
From JSX To A Block Factory
Take a small component:
import { ref } from 'vue'
export default () => {
const count = ref(0)
const ok = ref(true)
return () => (
<div class={{ active: ok.value }} onClick={() => count.value++}>
count: {count.value}
</div>
)
}In Virtual DOM mode this becomes a block VNode. In Vapor mode, the shape is closer to this:
const _t0 = _template("<div> ", 1)
return () => (() => {
const _n0 = _t0()
_on(_n0, "click", () => count.value++)
const _x0 = _txt(_n0)
_setNodes(_x0, "count: ", () => count.value)
_renderEffect(() => _setClassName(_n0, ok.value ? 1 : 0, "active"))
return _n0
})()There is no VNode diff for the text or class. The template creates the DOM once. The compiler finds the text node. count.value updates that text position. ok.value updates that class bit.
What "Real Local Update" Means
Vapor local update means the unit of reactivity is the operation, not the component render function.
The compiler builds a Vapor IR with operation nodes such as SetText, SetProp, SetNodes, CreateComponent, If, For, SlotOutlet, and Key. Then it decides whether each operation can run once or must be wrapped in renderEffect.
If a prop value is static, it goes into the HTML template string. If it is dynamic, it becomes a targeted setter. If a text run contains dynamic values, the compiler reserves an anchor or text child and emits setNodes or setText.
At runtime, setNodes resolves only the values passed to that position. Dynamic functions are tracked with renderEffect; when they change, the previous node or fragment is replaced or updated around the same anchor. The rest of the component does not re-run just to rediscover the same DOM structure.
Static HTML, Dynamic Islands
The Vapor compiler keeps a template buffer while walking JSX. Native elements with static attributes are stringified:
<button type=button> </button>The generated code then calls Vue's template helper and accesses dynamic children with direct paths:
const _n0 = _t0()
const _x0 = _txt(_n0)For deeper nodes, code generation uses _child, _next, and _nthChild. Adjacent dynamic nodes can reuse a cursor instead of repeatedly asking the parent for indexed children. That is a small detail, but it shows the overall philosophy: once the compiler knows the DOM shape, runtime should not rediscover it.
Control Flow Is Compiled Too
v-if does not become "call the render function again and diff whatever comes out." It becomes createIf(condition, positiveBlock, negativeBlock, flags). The flags encode branch shapes, v-once, slot-root behavior, and whether a branch can skip its own EffectScope.
v-for becomes createFor(source, block, getKey, flags). The compiler passes shape information such as "the list is the only child", "the item is a component", "the block is a single node", or "the block is a fragment". With a key, the compiler can also create selectors for patterns like item.id === id, so effects inside list items can be reset only when the relevant key changes.
In other words, Vapor does not remove the compiler. Vapor needs the compiler even more, because the compiler is what turns declarative JSX into precise DOM operations.
Why It Still Feels Functional
The author-facing model is still a function. A component receives props and context, closes over reactive state, and returns a block result. There is no instance proxy or this-based render contract in the mental model.
The non-functional part, mutating the DOM, is generated by the compiler and isolated into small operations. The code you write describes a value. The code the compiler emits knows how to maintain that value in the DOM.
That is the "real functional programming" angle of Vapor JSX: components stay composable functions, while effects are made explicit, local, and generated.
The Practical Payoff
Vapor mode is ideal for interfaces with frequent localized updates: counters, forms, dashboards, editable rows, live data, and animation-adjacent UI. The static DOM is created once, and each reactive read updates the place where it is used.
Virtual DOM mode remains the compatibility default. Vapor is an opt-in output mode through vapor: true, .vapor.tsx, .vapor.jsx, defineVaporComponent, or defineVaporCustomElement. That makes it possible to adopt real local updates incrementally.