SolidJS Port: gzip 8.33 kB, React −83%, Because Fine-Grained Reactivity Means No Virtual DOM
Solid looks like React — JSX, hooks-shaped APIs — but underneath it has no virtual DOM and no re-rendering. Your component function runs once. Subscriptions are wired at compile time. For this landing page, that difference takes the bundle from 49 kB (React) to 8.33 kB. Same features, same styles, same tests.
Entry #4 in the framework comparison series. After React (49 kB), Vue (28.76 kB), and Svelte (18.92 kB), Solid lands at 8.33 kB, making it the smallest result so far and an 83% reduction from the React baseline. The wild part is that the code reads almost identically to React.
🔗 Live demo: https://sen.ltd/portfolio/portfolio-app-solid/
📦 GitHub: https://github.com/sen-ltd/portfolio-app-solid
Feature parity with every other port. Shared code (filter.ts, types.ts, data.ts, style.css, tests/filter.test.ts) is byte-identical with the React version.
createSignal, createMemo, onMount
The API surface is deliberately familiar:
import { createSignal, createMemo, onMount, Show, For } from 'solid-js'
import type { PortfolioData, Lang } from './types'
import { loadPortfolioData } from './data'
import { filterAndSort, type FilterState } from './filter'
import { MESSAGES, detectDefaultLang } from './i18n'
export function App() {
const [status, setStatus] = createSignal<'loading' | 'error' | 'ready'>('loading')
const [errorMsg, setErrorMsg] = createSignal('')
const [data, setData] = createSignal<PortfolioData | null>(null)
const [lang, setLang] = createSignal<Lang>(detectDefaultLang())
const [filter, setFilter] = createSignal<FilterState>({
query: '', category: 'all', stack: 'all', stage: 'all', sort: 'number',
})
onMount(() => {
loadPortfolioData()
.then((d) => { setData(d); setStatus('ready') })
.catch((e) => { setErrorMsg(String(e)); setStatus('error') })
})
const visible = createMemo(() => {
const d = data()
return d ? filterAndSort(d.entries, filter(), lang()) : []
})
return <Show when={data()} fallback={<div>Loading...</div>}>{/* ... */}</Show>
}
React developers will feel at home. But the two crucial differences are:
-
data()is a function call, not a direct variable read. That's how Solid knows who's reading. - The component function runs exactly once. Not on state changes, not on prop changes. Once.
Both of those are the mechanism behind the bundle-size result.
Why "function-call reads" matter
In React, const visible = useMemo(() => filterAndSort(...), [filter]) still re-runs the entire component function whenever filter changes. useMemo only caches the inner computation's output; the outer render runs regardless.
In Solid, createMemo(() => { ... filter() ... }) creates a subscription at the site of filter(). When you setFilter(...), Solid re-runs only the memo — not the component, not the DOM pieces that don't depend on it. The component function is called once during mount, and its output is a graph of subscriptions, not a render tree.
export function App() {
const [count, setCount] = createSignal(0)
console.log('render') // logs ONCE
return <div>{count()}</div>
// this {count()} expression re-runs on each setCount,
// but the component function does not
}
That's "fine-grained reactivity" in a sentence: reactivity happens inside the compiled output, at the granularity of DOM assignments, not at the granularity of component re-runs.
Bundle math
dist/assets/index-<hash>.js 22.21 kB │ gzip: 8.33 kB
The 8.33 kB is roughly:
- Solid's runtime (~7 kB) — signal, effect, DOM updates. No virtual DOM, no reconciler.
-
Compiled template output — JSX becomes direct
document.createElement(...)calls with signals wired to specific property assignments. - No re-render machinery — the component function runs once, so a large class of React-era machinery simply doesn't exist in the bundle.
When you compare against React's 49 kB, what's missing is reconciliation, fiber scheduling, the event system, and the "stateful functional component" machinery. React ships all of that because it enables its mental model. Solid declines that mental model and ships a different, smaller runtime.
<Show> and <For> as compiler hints
Solid uses special components for control flow instead of inline JS expressions:
<Show when={data()} fallback={<div class="state state-loading">{MESSAGES[lang()].loadingLabel}</div>}>
{(d) => (
<>
<header class="site-header">{/* ... */}</header>
<main>
<For each={visible()}>
{(entry) => (
<EntryCard
entry={entry}
lang={lang()}
stackMap={stackMap()}
stageMap={stageMap()}
categoryMap={categoryMap()}
/>
)}
</For>
</main>
</>
)}
</Show>
<Show when={...}> is a safe version of {condition && <...>} that compiles to a proper reactive branch. <For each={...}> is the reactive analog of .map() that keeps DOM nodes stable across reorderings — basically key support baked in without you having to think about it.
The upshot: same JSX mental model React developers know, but every control flow construct is compile-time-aware and emits efficient reactive code.
Byte-identical shared files
$ diff repos/portfolio-app-react/src/filter.ts repos/portfolio-app-solid/src/filter.ts
# no output
$ diff repos/portfolio-app-react/src/types.ts repos/portfolio-app-solid/src/types.ts
# no output
$ diff repos/portfolio-app-react/src/style.css repos/portfolio-app-solid/src/style.css
# no output
Shared code doesn't change. What changed between React (49 kB) and Solid (8.33 kB) is exclusively the framework layer.
Tests
14 Vitest cases, all on filter.ts:
$ npm test
✓ tests/filter.test.ts (14 tests) 8ms
Zero framework-specific tests. Logic is in pure functions, so Solid's runtime is entirely absent from the test surface.
Scoreboard so far
| Port | gzip | vs React | Note |
|---|---|---|---|
| 021 React | 49.00 kB | — | VDOM baseline |
| 022 Vue | 28.76 kB | −41% | Proxy reactivity |
| 023 Svelte | 18.92 kB | −61% | Compile-heavy |
| 024 Solid | 8.33 kB | −83% | Fine-grained, no VDOM |
Solid sits in an interesting position: it offers the JSX experience React developers already know but skips the cost of virtual-DOM reconciliation. For read-heavy UIs like this landing page, it's the best ergonomics-to-bundle-size tradeoff in the series so far.
Series
This is entry #24 in my 100+ public portfolio series, and #4 in the framework comparison.
- 📦 Repo: https://github.com/sen-ltd/portfolio-app-solid
- 🌐 Live: https://sen.ltd/portfolio/portfolio-app-solid/
- 🏢 Company: https://sen.ltd/
Next up: Nuxt 3 (025). The only port in the series that ships more bytes than React. The reason is interesting.

Top comments (0)