Why SolidJS Signals Deliver Surgical DOM Updates – A Practical Guide
SolidJS’s Signals let you update the DOM surgically—no virtual‑DOM diffing, no component re‑renders. Learn how signals, memos, and control flow primitives work, see a worked example, and weigh the trade‑offs for real‑world projects.
06 Sept 2026, 12:04 UTC

Problem: Re‑rendering Overhead in Component‑Based UI Libraries
When a UI library updates the view after a state change, it usually re‑executes the component function and then runs a diff algorithm against the virtual DOM. This process can be expensive, especially when only a single element changes. Developers often resort to memoization tricks or manual batching, but the underlying issue remains: the library has to decide what changed and how to apply it.
Thesis: SolidJS’s Signals Provide Precise, No‑Diff DOM Updates
SolidJS replaces the virtual‑DOM diffing step with a fine‑grained reactivity system built around Signals. A Signal is a simple getter/setter pair that automatically tracks dependencies when accessed inside a reactive context (e.g., createEffect, createMemo, or JSX). When a signal changes, only the subscribers that read that signal are notified, and the compiler generates direct DOM instructions for those subscribers. The result is surgical updates: the component function runs once at mount, and subsequent state changes touch only the affected nodes.
Section 1 – Signals in Action
Below is a minimal example that demonstrates how a counter updates the DOM without re‑executing the component body.
import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0); // getter/setter pair
return (
<div>
<button onClick={() => setCount(count() + 1)}>Increment</button>
<p>Clicked {count()} times</p>
</div>
);
}
When setCount is called, the compiler knows that only the text node inside the <p> depends on count(). It emits a single DOM update that replaces that text node’s content, leaving the rest of the component untouched. No re‑render of Counter occurs, no diffing, no virtual‑DOM allocation.
Section 2 – Derived State with createMemo
Computations that depend on signals can be cached with createMemo. The memo only recomputes when its dependencies change, and it returns a signal that can be read like any other.
const [count, setCount] = createSignal(0);
const double = createMemo(() => count() * 2);
Here double() will recompute only when count() changes, and any component using double() will subscribe to that memo’s signal. The compiler generates a single subscription chain, ensuring that expensive calculations are performed only when necessary.
Section 3 – Control Flow Primitives
SolidJS offers reactive primitives like <Show>, <For>, and <Switch>. These components subscribe directly to signals and manipulate the DOM tree without key reconciliation.
const [items, setItems] = createSignal(["apple", "banana"]);
return (
<For each={items()}>
{(item) => <li>{item}</li>}
</For>
);
When setItems is called, Solid inserts or removes <li> nodes as needed, based on the array length. No key reconciliation is performed, which keeps the update cost linear in the number of changed items.
Section 4 – Batching with batch
When multiple signals change in quick succession, grouping the writes into a batch ensures that downstream effects and memos run only once per event loop tick.
import { batch } from "solid-js";
function updateAll() {
batch(() => {
setA(1);
setB(2);
setC(3);
});
}
Without batch, each write would trigger its own micro‑task, potentially causing unnecessary recomputations.
Trade‑Offs and Limitations
- No Virtual DOM Runtime – While this reduces overhead, it also means that developers cannot rely on a diffing algorithm to reconcile complex UI changes. Complex dynamic layouts may require careful use of control flow primitives.
- Signal Reads Outside Reactive Contexts – Reading a signal in a plain callback (e.g.,
setTimeoutor event handler) yields a one‑time value; the callback will not re‑run when the signal changes. - Destructuring Props Breaks Reactivity – Doing
const { count } = propscreates a plain value; useprops.count()ormergePropsto preserve reactivity. - SSR Hydration – Signals must hold serializable values. Functions or non‑JSON‑serializable objects can cause hydration mismatches.
- Third‑Party DOM Mutators – Libraries that mutate the DOM directly (e.g., D3 charts) must be wrapped in
createEffectwith anonCleanupto avoid memory leaks.
Practical Check – Verifying Surgical Updates
- Run the example in a browser and open DevTools.
- Trigger the counter button and observe the network panel – no component code is re‑executed.
- Use the Performance tab: record a signal update and confirm that only the
<p>text node is modified. - Inspect the generated code in
dist/after a build; it should contain direct DOM mutation calls rather than a virtual‑DOM diff.
Actionable Closing – When to Use SolidJS Signals
If your application has frequent, fine‑grained state updates (e.g., real‑time dashboards, interactive forms, or complex animations), SolidJS’s signal system can provide a performance advantage over libraries that rely on virtual‑DOM diffing. For simpler applications or when you need a mature ecosystem of component libraries, the trade‑off may be less clear.
To get started, scaffold a new Solid project with Vite:
npm create solid@latest my-app -- --template=ts
cd my-app
npm run dev
From there, experiment with signals, memos, and control flow primitives to see the surgical DOM updates in action. Keep the limitations in mind, and you’ll be able to design UIs that are both responsive and efficient.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.