Solid.js takes a different approach to UI updates: instead of re-running component trees, it updates the exact DOM nodes affected by a state change. If you're evaluating Solid for a content-driven app, the details that matter are how its reactive model works, where SolidStart fits, and how it pairs with Strapi for server-rendered, API-driven builds.
Most JavaScript frontend frameworks re-run component logic broadly when a single value changes. Solid.js doesn't re-render components at all.
In brief:
- Solid.js components run once as setup functions, then never re-execute. Only the specific DOM nodes subscribed to a signal update when that signal changes.
- Fine-grained reactivity eliminates the virtual DOM,
useMemo,useCallback, and dependency arrays. The reactive graph handles updates automatically. - On the benchmark, Solid consistently performs within ~7% of vanilla JavaScript, with an overall geometric mean of ~1.07×.
- SolidStart, the companion meta-framework, provides SolidStart docs. It also includes server functions and deployment presets for many targets.
Solid.js is a declarative JavaScript UI library that uses fine-grained reactivity to update only the exact DOM nodes affected by a state change. No virtual DOM, no diffing, and no component re-execution. Created by Ryan Carniato, it has been under active development for about a decade, with v1.x as the stable release line and v2.0 in beta.
Signals have influenced the broader JavaScript ecosystem. The Svelte signals work and the TC39 proposal show how central this model has become.
What Is Solid.js?
At its core, Solid is a reactive UI library, not a full framework, that uses JSX and compiles it to direct DOM operations at build time. If you've worked with JSX before, the syntax will look familiar: functional components, JSX, and createSignal instead of useState. The execution model underneath is the part you need to pay attention to.
Components run once as factory functions to set up a reactive graph, then never re-execute. Only the specific DOM nodes subscribed to a signal update when that signal changes. There's no virtual DOM, no reconciliation pass, and no diffing algorithm.
The official docs describe Solid as "a JavaScript library built around signals" that "prioritizes a simple and predictable development experience."
The bundle footprint reflects this minimalism: Solid's core runtime is roughly 7–7.6 KB. It's TypeScript-first. Solid is the UI layer; SolidStart is the companion meta-framework for server-side rendering, routing, and server functions.
How Fine-Grained Reactivity Works
This is the concept that defines Solid. If you're trying to understand what "Solid.js explained" means in practice, this is the part that affects how you build components, fetch data, and avoid unnecessary updates.
Signals, Memos, and Effects
Solid's reactive system is built on three primitives. From the reactivity docs:
createSignal returns a getter function and a setter. The getter must be called to read the value, and Solid tracks which computations depend on it at runtime. This function-call API is the mechanism of the entire reactivity model, not stylistic convention.
createMemo creates derived values that recompute only when their dependencies change. If a dependency changes but the computed output is identical (by ===), downstream computations are not notified. This acts as a natural optimization boundary.
createEffect runs side effects when tracked signals update. There are no manual dependency arrays. Solid tracks dependencies automatically based on which signals you actually read inside the effect.
Here's how they work together:
import { createSignal, createMemo, createEffect } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
const doubled = createMemo(() => count() * 2);
createEffect(() => {
console.log(`Count is ${count()}, doubled is ${doubled()}`);
});
return (
<div>
<p>Count: {count()}</p>
<p>Doubled: {doubled()}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}The component function body runs exactly once. The <div>, <p>, and <button> elements are instantiated as real DOM nodes and never reconstructed. When setCount fires, only the text node bound to {count()} and the text node bound to {doubled()} update. The button, the parent <div>, and the component do not re-execute.
This is what Ryan Carniato identifies as a key differentiator: "components don't rerun".
Why No Virtual DOM?
Solid's model is straightforward: when state changes, signals notify their subscribed dependencies, and Solid updates the relevant DOM expressions directly without a virtual DOM diff.
No intermediate tree construction or general reconciliation pass in Solid's core rendering model, though Solid does provide a reconcile utility for store updates that performs diffing when needed. The signal subscription graph is the update mechanism. It's part of a broader shift away from heavy runtime reconciliation — Astro's Islands architecture attacks the same overhead problem through partial hydration. The official docs say that Solid "sets itself apart by using JSX immediately as it returns DOM elements."
The benchmark data backs this up. On the benchmark, Solid v1.9.3 consistently benchmarks near vanilla JavaScript:
| Operation | Vanilla JS | Solid v1.9.3 | Other library |
|---|---|---|---|
| Create 1,000 rows | 22.2 ms (1.00×) | 23.0 ms (1.04×) | 30.8 ms (1.39×) |
| Replace all 1,000 rows | 24.1 ms (1.00×) | 26.1 ms (1.08×) | 32.4 ms (1.34×) |
Core Solid.js Features
These are the primitives you'll reach for on top of signals: JSX compilation, control flow helpers, stores for nested state, and resources for async data. Each preserves fine-grained reactivity — the framework features don't paper over the reactive model, they extend it.
JSX That Compiles to Real DOM
Solid uses JSX but compiles it differently than many UI libraries. The JSX docs state: "Solid sets itself apart by using JSX immediately as it returns DOM elements."
The compiler extracts static HTML into cloned template nodes and wraps dynamic expressions in fine-grained subscriptions. Static props integrate into cloned template nodes at build time; dynamic expressions compile to reactive bindings targeting specific DOM nodes.
The JSX return in a component "creates a tracking scope behind the scenes, which allows signals to be tracked within the return statement of a component," according to the intro docs. This means Solid's JSX is a compile target, not a runtime abstraction.
Control Flow Components
Solid provides <Show>, <For>, <Switch>/<Match>, and <Suspense> as control flow. These are not just syntax helpers. They preserve fine-grained tracking.
<For> tracks each item by reference and only updates the specific row that changed. Solid also provides <Index> for positional tracking when working with primitive arrays.
The distinction matters: <For> keys DOM nodes to item identity, while <Index> keys to position. The list rendering docs explain that <For> is intended for complex data structures with stable identity, while <Index> is a better fit when list order and length stay stable but values change frequently.
<Show> is used for conditional rendering. When the when condition changes, only the content inside <Show> is re-evaluated, not the surrounding tree. It also solves a TypeScript narrowing problem that plain && expressions can't handle.
<Suspense> handles async loading boundaries, rendering a fallback until pending async dependencies resolve.
Stores for Nested State
For complex state beyond simple signals, Solid provides createStore, a proxy-based reactive object where individual property accesses are tracked. From the docs: "Stores maintain fine-grained reactivity by updating only the properties that change."
const [store, setStore] = createStore({ users: [{ name: "Alice", loggedIn: false }] });
// Surgical path-based update
setStore("users", 0, "loggedIn", true);Updating store.users[0].loggedIn only re-renders the DOM nodes reading that specific path, not everything consuming the store. This replaces the need for external state management libraries for most use cases.
One production behavior to know: store signals are created lazily, only formed when accessed within a tracking scope. Accessing a store property outside a reactive context won't be tracked, so changes to it won't trigger reactive updates.
Resource and Async Handling
createResource wraps async data fetching into the reactive system. It returns a signal-like accessor that integrates with <Suspense> for loading states. Data loaded via resources participates in the same fine-grained graph: when the resource resolves, only the consuming DOM nodes update.
A production detail worth knowing: resource() inside <Suspense> triggers suspension when pending, while resource.latest returns the last resolved value without triggering suspension. This lets you show stale data during re-fetch instead of reverting to a loading spinner.
createAsync is the recommended async primitive for most asynchronous data fetching in Solid's router/data APIs, and it is intended to become the standard async primitive in a future Solid 2.0 release, particularly in SolidStart route-related data loading.
SolidStart: The Full-Stack Meta-Framework
Solid on its own is just the UI layer. SolidStart is what you reach for when you need routing, SSR, server functions, and a deployment story to go with it.
What SolidStart Adds
SolidStart adds file-based routing, server-side rendering (SSR), Static Site Generation (SSG), server functions, and deployment presets. The official docs describe it as "an open source meta-framework designed to unify components that make up a web application."
It uses Vite-based tooling and deployment presets, so it can target many platforms through configuration. SolidStart is deliberately minimal and modular. It ships without a built-in router, and its docs use Solid Router in examples while allowing you to use the router of your choice.
The current stable version is 1.3.2, with a 2.0 alpha under active development.
Server Functions and Single-Flight Mutations
Mark any function with "use server" and it executes on the server, callable from the client without manual API wiring.
The standout feature is single-flight mutations. From the docs: "Traditionally, this is done in two separate HTTP requests: one to update the data, and a second to fetch the new data. Single-flight mutations are a unique feature of SolidStart that handles this pattern in a single request."
When a POST request occurs, because the router has context about the current and next location, the response for the next page is returned while data is fetched in parallel and streamed back, all within one round-trip.
Rendering Modes
SolidStart supports Client-Side Rendering (CSR), SSR, and SSG from a single codebase. Streaming SSR sends HTML chunks as data resolves, which helps with slow APIs or edge deployments. Deployment configuration depends on the target platform and typically uses platform-specific plugins or setup rather than a single Vite server.preset switch.
Building with Solid.js: Quick Start
Enough theory. Here's what you need to scaffold a project and get a first reactive component on the page.
Project Setup
For a frontend Single-Page Application (SPA) without server-side rendering (SSR):
npm init solid
# Select: No to SolidStart, ts template, TypeScript
cd solid-project
npm install
npm run devFor a full-stack project with SSR, server functions, and deployment adapters:
npm init solid@latest
# Select: Yes to SolidStart, basic template, TypeScriptBoth scaffold a Vite-based project. Available template variants from the create-solid CLI include ts, ts-vitest, ts-tailwindcss, and more, though the solidjs/templates GitHub repo uses different template directory names.
Creating Reactive Components
Here's a practical example: a component that fetches posts from an API, displays them, and filters by keyword.
import { createSignal, createResource, Suspense, For } from "solid-js";
function PostList() {
const [filter, setFilter] = createSignal("");
const [posts] = createResource(filter, async (keyword) => {
const url = keyword
? `/api/posts?search=${keyword}`
: "/api/posts";
const res = await fetch(url);
return (await res.json()).data;
});
return (
<div>
<input
placeholder="Filter posts..."
onInput={(e) => setFilter(e.target.value)}
/>
<Suspense fallback={<p>Loading posts...</p>}>
<For each={posts()}>
{(post) => (
<article>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
)}
</For>
</Suspense>
</div>
);
}Solid.js and Strapi: Headless CMS Integration
Strapi can be used as a headless CMS with SolidStart. As a headless CMS, Strapi provides the content backend while Solid handles the reactive frontend. For a step-by-step walkthrough, see this Solid.js + Strapi tutorial.
In a SolidStart project, you can create a server function that fetches articles from Strapi via the REST API, keeping your API token off the client entirely:
// src/lib/strapi.ts
import { query } from "@solidjs/router";
const STRAPI_URL = process.env.STRAPI_URL || "http://localhost:1337";
const STRAPI_TOKEN = process.env.STRAPI_API_TOKEN;
export const getArticles = query(async () => {
"use server";
const res = await fetch(`${STRAPI_URL}/api/articles?populate=*`, {
headers: { Authorization: `Bearer ${STRAPI_TOKEN}` },
});
if (!res.ok) throw new Error("Failed to fetch articles");
const json = await res.json();
return json.data; // v5 flat format, no json.data.attributes wrapper
}, "articles");Note the v5 format: the attributes wrapper from v4 is removed. Read json.data directly.
In a route component, createAsync loads the data reactively with route preloading, so the fetch fires in parallel with the route render:
// src/routes/articles/index.tsx
import { createAsync } from "@solidjs/router";
import { getArticles } from "~/lib/strapi";
import { Suspense, For } from "solid-js";
export const route = {
preload: () => getArticles(),
};
export default function ArticlesPage() {
const articles = createAsync(() => getArticles());
return (
<Suspense fallback={<p>Loading...</p>}>
<For each={articles()}>
{(article) => (
<article>
<h2>{article.title}</h2>
<p>{article.slug}</p>
</article>
)}
</For>
</Suspense>
);
}That pattern — server function, createAsync preload, <For> render — is the spine of most Solid + Strapi routes. The reactive graph does the rest: when the articles resource resolves, only the DOM nodes bound to each article's title and slug mount, and nothing outside the <For> re-executes. Add a search signal, a "use server" mutation, or a draft-preview toggle, and the shape of the code stays the same.
Wrapping Up
Solid.js is worth evaluating when you need predictable update cost on content-heavy pages and want to skip the mental overhead of manual memoization. The fine-grained model isn't magic — it's a different trade-off, not a free performance win — but once the reactive graph is set up, the code you write tends to be shorter and closer to what the DOM actually does.
Pair it with Strapi for the content layer and SolidStart for routing, and you have a stack where both pieces stay small and the update cost stays predictable.