SolidJS Port: gzip 8.33 kB, React 83%, Because Fine-Grained Reactivity Means No Virtual DOM

typescript dev.to

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>
}
Enter fullscreen mode Exit fullscreen mode

React developers will feel at home. But the two crucial differences are:

  1. data() is a function call, not a direct variable read. That's how Solid knows who's reading.
  2. 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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The 8.33 kB is roughly:

  1. Solid's runtime (~7 kB) — signal, effect, DOM updates. No virtual DOM, no reconciler.
  2. Compiled template output — JSX becomes direct document.createElement(...) calls with signals wired to specific property assignments.
  3. 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>
Enter fullscreen mode Exit fullscreen mode

<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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Next up: Nuxt 3 (025). The only port in the series that ships more bytes than React. The reason is interesting.

Source: dev.to

arrow_back Back to Tutorials