DEV Community

SEN LLC
SEN LLC

Posted on

Astro Port: 3.17 kB Gzip, 94% vs React, New Series Record — Because the Framework Runtime Is Literally Zero

Astro Port: 3.17 kB Gzip, −94% vs React, New Series Record — Because the Framework Runtime Is Literally Zero

Previous series best was Solid at 8.33 kB. I did not think you could go meaningfully lower for this spec. Astro came in at 3.17 kB — a 94% reduction from React. The trick is "no framework runtime at all": the only JavaScript in the bundle is the application code itself, because Astro doesn't insert a rendering layer between your code and the DOM.

Entry #8 in the framework comparison series. Running totals so far: React 49.00 kB, Vue 28.76 kB, Svelte 18.92 kB, Solid 8.33 kB, Nuxt 52.01 kB, SvelteKit 32.50 kB, Qwik first-paint 28.60 kB. Then Astro lands at 3.17 kB and the scoreboard has a new floor.

🔗 Live demo: https://sen.ltd/portfolio/portfolio-app-astro/
📦 GitHub: https://github.com/sen-ltd/portfolio-app-astro

Screenshot

Same spec, same features, same styles, same tests as the other eight ports. Shared code (filter.ts, data.ts, types.ts, style.css, tests/filter.test.ts) is byte-identical with the React version.

Astro's "no runtime" philosophy

Astro is built on a very specific principle: by default, send zero JavaScript to the browser. Pages are written in .astro files, processed at build time to produce HTML, and shipped without a framework runtime attached. JavaScript is introduced only in explicitly marked "islands" that you opt into:

---
import Layout from '../layouts/Layout.astro'
const data = await loadPortfolioDataAtBuildTime()
---

<Layout title="SEN Portfolio">
  <main>
    {/* Static HTML — no JS */}
    {data.entries.map((entry) => (
      <article class="card">
        <h2>{entry.name.ja}</h2>
      </article>
    ))}

    {/* Opt-in island */}
    <FilterControls client:load entries={data.entries} />
  </main>
</Layout>
Enter fullscreen mode Exit fullscreen mode

Only <FilterControls client:load> compiles to client JS; everything else is pure HTML.

But this app is entirely interactive — the filter/search/sort UI spans the whole page. Making the whole thing an island would bring back framework overhead. So I went the other direction.

The approach: no islands, just vanilla <script>

I wrote the interactive logic as a single <script> block at the bottom of the Astro page, using vanilla TypeScript:

---
import Layout from '../layouts/Layout.astro'
---

<Layout title="SEN Portfolio">
  <header class="site-header">...</header>
  <main>
    <section class="controls">
      <input id="search" type="text" />
      <select id="filter-category">...</select>
    </section>
    <section id="grid" class="grid"></section>
  </main>
</Layout>

<script>
  import { loadPortfolioData } from '../data'
  import { filterAndSort, type FilterState } from '../filter'
  import { MESSAGES, detectDefaultLang } from '../i18n'

  const lang = detectDefaultLang()
  let data = await loadPortfolioData()
  let filter: FilterState = { query: '', category: 'all', stack: 'all', stage: 'all', sort: 'number' }

  function render() {
    const visible = filterAndSort(data.entries, filter, lang)
    document.getElementById('grid')!.innerHTML = visible
      .map((entry) => `<article class="card">...</article>`)
      .join('')
  }

  document.getElementById('search')!.addEventListener('input', (e) => {
    filter.query = (e.target as HTMLInputElement).value
    render()
  })
  // more listeners...
  render()
</script>
Enter fullscreen mode Exit fullscreen mode

Astro's <script> tag goes through Vite at build time, which means full TypeScript support, module imports, and tree-shaking. What makes this magical is what's not in the output: no React, no Vue, no Svelte runtime. Just the code in that script block.

Client JS = filter.ts + data.ts + i18n.ts + ~50 lines of DOM manipulation = 3.17 kB gzip.

This is where the shared-code discipline pays off

All nine ports share filter.ts (pure functions, no framework). In the React/Vue/Svelte/Solid ports, the bundle contains filter.ts plus the framework's component runtime to orchestrate calls to it. In the Astro port, there's no component runtime — the DOM manipulation is explicit, and the bundle is just filter.ts plus the handful of lines that wire events to render().

The series constraint (byte-identical shared code) has been producing fair comparisons all along. Here it produces the most compressed possible implementation of the same spec — every byte measures something real.

HTML grows, but that's the trade

One nuance worth calling out: Astro ships bigger HTML as a consequence of rendering more at build time. index.html is a few kilobytes larger than, say, the React port's essentially-empty shell. So "total transfer size" is less impressive than "JS-only size."

This is deliberate. The series scoreboard measures gzipped JS because parsing and executing JS is much more expensive per byte than parsing HTML, especially on slow devices. A 1 kB increase in JS has perhaps 10× the runtime impact of a 1 kB increase in HTML. Astro's trade — more HTML, far less JS — is the right one for a content-driven page.

Why this works here specifically

The app has a single interactive loop: filter state changes, and the grid re-renders. That pattern doesn't need virtual DOM, doesn't need fine-grained reactivity, doesn't need lifecycle hooks. A single render() function that runs on every state change is sufficient.

If this were an app with complex state transitions, animations, form validation, or deeply nested interactive trees, the vanilla approach would get unwieldy fast, and a framework would earn its overhead. Framework weight should scale with application complexity — and this app is at the "very simple" end, so Astro wins.

Shared files

$ diff repos/portfolio-app-react/src/filter.ts repos/portfolio-app-astro/src/filter.ts
# no output

$ diff repos/portfolio-app-react/src/types.ts repos/portfolio-app-astro/src/types.ts
# no output

$ diff repos/portfolio-app-react/src/style.css repos/portfolio-app-astro/src/style.css
# no output
Enter fullscreen mode Exit fullscreen mode

Tests

14 Vitest cases on filter.ts. Same test file as every other port; framework-free.

Scoreboard so far

Port gzip vs React
021 React 49.00 kB
022 Vue 28.76 kB −41%
023 Svelte 18.92 kB −61%
024 Solid 8.33 kB −83%
025 Nuxt 52.01 kB +7%
026 SvelteKit 32.50 kB −33%
027 Qwik 28.60 kB (first-paint) −42%
028 Astro 3.17 kB −94% ← new record

Series

This is entry #28 in my 100+ public portfolio series, and #8 in the framework comparison.

Next: Lit 3 Web Components (029). I thought Lit might match or beat Astro. It didn't — 9.70 kB, on par with Solid — but it's an interesting architectural choice worth its own writeup.

Top comments (0)