Fine-Grained Updates in SolidJS: Signals Instead of Virtual DOM Reconciliation
SolidJS compiles JSX to direct DOM updates wired to signals. Components run once, and only the specific DOM nodes that depend on a changed signal update, avoiding virtual DOM reconciliation.
04 Aug 2025, 01:21 UTC

The problem is familiar: a small piece of state changes, and a framework re-evaluates a whole component tree to figure out what to patch. In SolidJS the engineering decision is to avoid that reconciliation step entirely. JSX is compiled to imperative DOM instructions that are wired directly to reactive primitives. The useful takeaway is that updates become node-level, not component-level.
Compile-time wiring, not runtime diffing
SolidJS uses a compiler to transform JSX into direct DOM manipulation. The output is not a virtual tree that is diffed on each render. The compiler records which DOM nodes depend on which signals at build time.
Components are setup functions that run once. They create the DOM structure once and return it. On subsequent state changes the component body does not re-run. This removes the overhead of tearing down and rebuilding virtual nodes.
Signals create a subscription graph to the DOM
State starts with createSignal. A signal is a getter/setter pair. Reading the getter during setup registers the current DOM location as a subscriber.
When the setter is called, only the subscribers that read that signal are notified. The effect system runs the minimal update code the compiler emitted for that node.
Derived values use createMemo. A memo tracks its dependencies and only recalculates when one of them changes. Multiple effects that depend on the same derived value share that calculation.
Worked example: counter with derived total
The following pattern shows the one-time setup and fine-grained wiring. Run it in a SolidJS project entry file where JSX compilation is enabled.
import { createSignal, createMemo, For } from 'solid-js';
function CounterList() {
const [count, setCount] = createSignal(0);
const doubled = createMemo(() => count() * 2);
return (
<div>
<button onClick="() => setCount(c => c + 1)">Increment</button>
<p>Count: {count()}</p>
<p>Doubled: {doubled()}</p>
<For each={[1,2,3]}>
{(n) => <span>Item {n} - base {count()}</span>}
</For>
</div>
);
}
Here count() is read three times during setup. The compiler creates three separate subscriptions. Clicking Increment updates only the text nodes that read count and the node that reads doubled. The component function itself is not invoked again.
Practical check: open the browser Elements tab and trigger the update. Observe that only the specific text nodes change, not the wrapper <div> or the <button>. A console.log placed in the component body runs once; a console.log inside a createEffect runs on dependency changes. This contrast demonstrates the one-time setup.
Trade-offs and common pitfalls
Destructuring props breaks reactivity. Accessing a prop value immediately with const { value } = props captures the value at setup time and loses the getter reference. Keep props as functions or access them via props.value inside reactive contexts.
Overusing nested signals without memos can cause redundant work. If several effects compute the same derived value from the same base signals, each effect will recompute independently. Centralize the computation in a createMemo so the work is shared and cached until dependencies change.
The model favors explicit dependency tracking. Developers need to be aware that reactivity is established by reading signals during setup, not by later mutations. This is a shift from frameworks where re-rendering implicitly re-reads props.
When to use this approach
Fine-grained reactivity is valuable for UIs with many independent, frequently updating nodes, such as live dashboards, large lists with per-item state, or forms with derived validation. The limitation is mental model cost: debugging requires understanding the signal graph rather than a render output.
Start by isolating state with signals, derive expensive values with memos, and avoid destructuring reactive inputs. Verify updates by inspecting DOM mutations and effect execution counts rather than assuming component re-renders.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.