Part of: Questions & Answers · The Story of Morph
No. Morph has no virtual DOM — and it doesn't need one. This page covers how React's model works, what Morph does instead, and why the virtual DOM wasn't the right tool here.
React's core trick is a JavaScript mirror of your UI. When your state changes, React rebuilds that JS mirror, diffs the old mirror against the new one, figures out the smallest set of DOM operations that get from one to the other, and applies just those operations to the real DOM.
That mirror exists for a reason. The browser DOM was never designed for fast, high-frequency updates — reading and writing it is expensive. So React keeps a cheap, in-JS copy of it, computes changes against that copy, and touches the real DOM as little as possible. The virtual DOM is a layer that exists to protect the real DOM from change.
Morph was never in that situation.
Morph is a compiler + native runtime, not a JS library running on a DOM. Your .mx source compiles into a real, native node tree that the runtime owns directly. That tree has no browser to protect — it is the actual UI resource, held in native memory and drawn with OpenGL.
In Morph, a state change flows through three native mechanisms instead of a diff pass:
morphState values are signals (`reactivity/signal.h`). Each signal keeps a list of subscriber effects, and set() notifies them directly. notify_all() marks each subscribed effect as pending and appends it to a pending queue (`reactivity/effect.cpp`):
void set(T v) {
std::lock_guard<std::mutex> lock(m_mutex);
value_ = v;
notify_all();
}Notice what's not here: no tree rebuild, no diff. Only the effects that actually subscribed to that value are queued — and subscription is automatic while an effect runs.
Each effect that writes the UI marks nodes instead of rebuilding them. Every node carries a set of flags (`core/node.h`):
enum DirtyFlag : uint8_t {
Clean = 0,
StyleDirty = 1 << 0,
LayoutDirty = 1 << 1,
PaintDirty = 1 << 2,
ScrollDirty = 1 << 3,
SubtreeDirty = 1 << 4,
};A style change marks PaintDirty; a geometry change marks LayoutDirty; any change propagates SubtreeDirty up to ancestors. Then the frame only redoes what's flagged:
layoutIfNeeded() skips clean subtrees entirely (`core/node/node.cpp`) and counts the skips, so you can see how much work was avoided. Dirty nodes relayout; the rest don't move.Downstream of dirty flags, the renderers decide what reaches the screen:
RenderFrame, present. Pixel-correct by construction (rendering/flash).PaintDirty nodes become damage rects each frame.Nothing in that chain compares one tree to another, because the tree is never duplicated in the first place. The node you mutate is the node that lays out and draws.
The virtual DOM solves a problem Morph doesn't have, and it would cost Morph things it can't afford:
RenderFrame. There's no JS scheduler to thread through.Morph is React-inspired at the API level — JSX, components, morphState, morphEffect, declarative UI. The familiar bits are deliberate. It's only the reconciliation machinery underneath that's absent, and deliberately so: that machinery exists for the browser, and Morph doesn't run in one.
See also Q: Is it React?, How It Works, and Rendering.