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
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>
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>
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
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.
- 📦 Repo: https://github.com/sen-ltd/portfolio-app-astro
- 🌐 Live: https://sen.ltd/portfolio/portfolio-app-astro/
- 🏢 Company: https://sen.ltd/
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)