- Vortrag: JUG Saxony Day 2026, Radebeul bei Dresden, 25. September 2026
- Sprecher: Vasco Schwarze (Software-Entwickler bei sciota GmbH) — Master HTW Dresden, viele Jahre Web-Entwicklung, größtenteils mit React
- Erfahrungshintergrund: Umbau der sciota-Website von WordPress auf Astro, seitdem weitere Projekte mit Astro
- Folien & Code: https://github.com/VascoSchwarze/do-you-really-need-an-spa · Kontakt: vasco.schwarze@sciota.io · GitHub: VascoSchwarze
Abstract
Websites laden im Median über 700 KB JavaScript, im 75. Perzentil über 1,2 MB — auch dann, wenn der Inhalt zu 99 % statisch ist. Der Vortrag stellt die zentrale Frage: „Brauchen wir dafür wirklich eine SPA?" — und beantwortet sie nicht dogmatisch, sondern anhand zweier funktional identischer Rezept-Apps: einmal als klassische React-SPA, einmal mit Astro (Islands Architecture, SSG).
Gezeigt werden die Messwerte beider Varianten (Build-Output, übertragene Datenmenge, Core Web Vitals), die Astro-Grundlagen (.astro-Syntax, Content Collections, Scoped CSS, i18n, file-based Routing), die vier interaktiven Features in reinem Vanilla-JS statt Framework — und der Schritt vom Client zum Server (SSR, Server Islands, Actions).
Worum es NICHT geht: reines Astro-Tutorial, Framework-Vergleiche (außer ein paar spezifischen Metriken), KI.
Agenda
- Einleitung: das JS-Bloat-Problem
- Warum SPAs zum Standard wurden — und was sie kosten
- Die Baseline-App (React-SPA) und ihre Messwerte
- Astros Philosophie: „Ship Zero JavaScript by Default"
- Der Astro-Nachbau (SSG) inkl. Interaktivität
- Vom Client zum Server: SSR, Server Islands, Actions
- Fragenkatalog fürs nächste Projekt
01. Einleitung. Das Problem
Beobachtungen
- JS-Gewicht im Web: im Median über 700 KB pro Website, im 75. Perzentil über 1,2 MB.
- Typisches Antipattern: Marketing-Seite als React/Vue-SPA gebaut, obwohl der Inhalt zu 99 % statisch ist (Text, Bilder, Kontaktformular). Der Nutzer lädt die komplette App-Runtime (React, ReactDOM, Router, State-Management), nur um denselben HTML-Text zu sehen, den ein Server direkt hätte ausliefern können.
- Kein Strohmann gegen React. Der rote Faden ist: das Werkzeug passt oft nicht zur Aufgabe.
Nicht der Erste, dem das auffällt
| Beitrag | Autor |
|---|---|
| JS-Bloat in 2024 | Nikita Prokopov |
| Second-guessing the modern web (2020) | Tom MacWright |
| Gegenangriff auf SPAs (2024) | Stranger Than Usual |
| justfuckingusehtml.com | — |
Die zentrale Frage
„Brauchen wir dafür wirklich eine SPA?" Oder geht es auch leichtgewichtiger?
Die Antwort ist manchmal „Ja". Der Vortrag ist ein Plädoyer für die bewusste Entscheidung statt den Reflex.
02. Ein kurzer Blick in die Vergangenheit
Warum wurden SPAs zum Standard?
SPAs sind keine reine Modeerscheinung:
- flüssigere UX — schnelle, responsive Navigation, App-ähnliches Gefühl
- Nutzung von Hardware-Fortschritten — bessere Endgeräte können mehr Code selbst ausführen
- geringere Serverlast — Rendering auf Client-Seite, weniger Netzwerkanfragen
- klares mentales Modell — Komponentenwiederverwendung statt großer HTML-Templates, klare Trennung Frontend/Backend (einfachere Cross-Platform-Anwendungen)
- riesiges Ökosystem — React/Vue/Angular mit ausgereiftem Tooling und großer Community
- Team-Gewohnheit — nach der Etablierung der Weg des geringsten Widerstands, unabhängig von technischer Eignung
Folge: SPAs wurden auch zum Standard für reine Content-Seiten.
Die technischen Kosten
- JS benötigt — ohne JS geht gar nichts.
- Größere Bundles — die Bundlegröße skaliert schlecht: neue Dependencies für eine Seite werden auf allen Seiten mitgeladen.
- Verzögertes Laden der Seite — reines CSR: Server liefert nur ein leeres HTML-Grundgerüst, sichtbarer Inhalt erst nach Download, Parsen und Ausführen des Bundles (sequentiell) — auf schwachen Mobilgeräten besonders kritisch.
- Schwieriges SEO
- Google (und zunehmend andere Suchmaschinen) crawlen SPAs über einen zusätzlichen Rendering-Schritt inzwischen fast so gut wie MPAs.
- Aber: nicht jeder Bot rendert JS — Social-Preview-Bots (Slack, X, LinkedIn) und Legacy-Tools sehen nur das leere initiale HTML.
- Und: einzelne Seiten lassen sich in einer SPA kaum unabhängig voneinander ranken; aktualisierter Inhalt wird langsamer entdeckt und re-indexiert.
- Accessibility-Einschränkungen
- Screenreader erkennen Seitenwechsel in SPAs oft nicht automatisch.
- Browser-integrierte Funktionen (Formulare, Routing, Historie) müssen nachgebaut und verwaltet werden.
- Mobile Daten kosten Geld — mehrere MB können für Nutzer teuer werden.
Lässt sich das messen? — Core Web Vitals
| Metrik | Zielwert | Was sie misst |
|---|---|---|
| LCP (Largest Contentful Paint) | < 2,5 s | Ladezeitpunkt des größten Content-Elements — bei CSR-SPAs abhängig von Bundle laden/parsen/ausführen plus Datenladen; jede Stufe addiert sich |
| INP (Interaction to Next Paint) | < 200 ms | Reaktionsgeschwindigkeit auf Klicks/Eingaben — leidet bei blockiertem Hauptthread |
| CLS (Cumulative Layout Shift) | < 0,1 | Visuelle Stabilität: verschieben sich Elemente nach dem ersten Render? |
Alle drei sind Google-definierte UX-Metriken und fließen ins Suchranking ein — also nicht nur UX-, sondern auch Business-relevant.
Warum interessiert uns das überhaupt?
Success Stories von Rakuten, BBC, Pinterest und Renault als Beleg:
- bessere UX → zufriedene Nutzer
- Conversion/Retention → bessere UX bringt messbar mehr Geld
- Nachhaltigkeit → geringere übertragene Datenmengen
03. Die Baseline-App: Rezeptbuch
Funktionsumfang
- Startseite mit ausgewählten Rezepten (fast reiner Content)
- Übersicht mit Textsuche & Kategorie-Filter (clientseitig, kein Reload)
- Detailseite mit Portionsrechner & Kochmodus-Checkliste
- Favoriten-Toggle mit
localStorage-Persistenz (app-weit)
Stack: React 19 + React Router 7 · Vite-Build · ein JS-Bundle für die komplette App.
Die Feature-Auswahl deckt bewusst das Spektrum ab: von „keine Interaktivität" bis „einfache lokale Interaktivität ohne geteilten State" — typisch für content-lastige Websites.
Technischer Aufbau
Routing (App.tsx) — alle Routen aus einer Komponente/einem Bundle; der Server liefert immer dasselbe minimale HTML, der Router entscheidet im Browser:
export default function App() {
return (
<RecipesProvider>
<Routes>
<Route element={<Layout />}>
<Route index element={<Home />} />
<Route path="rezepte" element={<Recipes />} />
<Route path="rezepte/:slug" element={<RecipeDetail />} />
<Route path="ueber-uns" element={<About />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</RecipesProvider>
);
}
Wichtig für später: Jede Seite — auch die simple Über-uns-Seite — lädt exakt dasselbe Bundle wie die interaktivste Seite. Es gibt kein Konzept von „weniger JS pro Seite".
Datenladen (context/RecipesContext.tsx) — die Rezeptdaten sind bewusst nicht ins Bundle gebündelt, sondern werden via fetch() aus einer statischen recipes.json zur Laufzeit geladen, inkl. echtem Ladezustand über React Context. Das ist realistisch: bei echten Daten aus DB/CMS könnte man ohnehin nicht statisch importieren.
Weitere Bausteine: pages/RecipeDetail.tsx (Portionsrechner via useState + useMemo, Kochmodus-Checkliste via Set<number>), hooks/useFavorites.ts (useSyncExternalStore über einem localStorage-Store).
Die Baseline-Messwerte
| Metrik | recipe-react (SPA) |
|---|---|
Build-Output gesamt (dist/) |
284,7 KB |
| LCP (Rezepteübersicht) | 146 ms |
| übertragene Datenmenge (Rezepteübersicht) | 89,8 KB |
| LCP („Über uns") | 68 ms |
| übertragene Datenmenge („Über uns") | 89,8 KB |
Hinweise zur Messung: Build-Output inkl. recipes.json, ohne Fotos; Messungen ohne Netzwerklatenz gegen einen lokalen Server auf leistungsstarkem Rechner; Übertragungsgrößen ebenfalls ohne Fotos.
Auffällig: Die Übertragungsgröße ist auf beiden Seiten identisch — die textlastige Über-uns-Seite lädt genauso viel wie die Rezepteübersicht.
04. Astros Philosophie
„Ship Zero JavaScript by Default"
- Jede Astro-Komponente wird zu Build- bzw. Request-Zeit zu statischem HTML/CSS gerendert; das dafür nötige JS wird verworfen.
- Umkehrung der SPA-Grundannahme: SPA = „alles braucht JS, bis das Gegenteil bewiesen ist"; Astro = „nichts braucht JS, bis es explizit als interaktiv markiert wird".
Islands Architecture
Eine Seite besteht aus statischen Bereichen (Sidebar, Hauptinhalt, Footer) und einzelnen interaktiven Inseln (z. B. Header, Karussell). JS wird nur für die Inseln geladen — wahlweise initial mit dem Rest der Seite oder erst, wenn das Element sichtbar wird.
Ähnliche Content-first-Ansätze
| Framework | Ansatz |
|---|---|
| Qwik | Resumability — serialisiert State ins HTML, setzt Ausführung im Client fort, ganz ohne Hydration-Schritt |
| htmx | HTML-Attribute statt Komponenten — hx-get/hx-post laden Server-HTML-Fragmente nach, kein virtueller DOM |
| Next.js | Partial Prerendering / React Server Components — Server Components laufen nur serverseitig |
| Astro | unser Beispiel heute |
Astro wurde als zugängliches Beispiel für ein größeres Denkmuster gewählt — nicht als einzig richtige Antwort.
Weitere Astro-Features
Scoped CSS · Content Collections · optimierte Bilder · Einbindung von JS-Framework-Komponenten · i18n · View Transitions
Anatomie einer .astro-Datei
Die .astro-Syntax ist eine Übermenge von HTML — reines HTML ist bereits eine gültige Astro-Komponente. Eine Datei hat zwei Zonen: das Frontmatter oben zwischen drei Bindestrichen (JS/TS: Imports, Typen, Props) und darunter das Template.
---
const names = ["John", "Paul"];
---
<main>
<h1>Rezepte</h1>
<ul>
{names.map(name => <li>{name}</li>)}
</ul>
</main>
Das Frontmatter läuft zur Build-Zeit (nie im Browser); es entsteht statisches HTML mit <li>John</li><li>Paul</li>.
Unterschiede zu JSX: normales class statt className, kein key-Prop bei Listen nötig, kein umschließendes Fragment nötig (mehrere Root-Elemente erlaubt). Bedingtes Rendern funktioniert wie in JSX ({condition && <div>…</div>}).
Komponenten, Slots, Props: Komponenten werden wie normale HTML-Tags verwendet, Props als Attribute; <slot /> markiert die Stelle für Kind-Inhalt (Pendant zu {children} in React); Astro.props enthält die übergebenen Props, typisiert über ein type Props.
05. Der Astro-Nachbau (SSG)
Dieselbe App — gleiche Seiten, Features und Daten — noch einmal mit Astro gebaut. Einziger Unterschied ist die Architektur (plus eine zusätzliche englische Version). Wichtig für die Benchmarks: Astro läuft hier im rein statischen Modus (SSG, kein Server zur Laufzeit).
File-based Routing
Die Ordnerstruktur unter src/pages/ definiert die Routen. Bei vielen Sprachen lässt sich die Struktur auch dynamisch in Bezug auf Sprachen aufbauen.
Pages und Layout
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import AboutView from '../../components/views/AboutView.astro';
import { useTranslations } from '../../i18n/utils';
const lang = 'de' as const;
const t = useTranslations(lang);
---
<BaseLayout lang={lang} title="Über uns" description={t('about.p1')}>
<AboutView lang={lang} />
</BaseLayout>
BaseLayout.astro ist eine normale Astro-Komponente mit <slot />, die den Kind-Inhalt in das Grundlayout (<head>, Header, <main>, globale Styles) rendert.
Scoped CSS
CSS, das nicht explizit als is:global markiert ist, wird lokal gescoped und leakt nicht in andere Komponenten. Astro hängt dafür beim Build ein data-astro-cid-*-Attribut an die Elemente und schreibt die Selektoren entsprechend um:
<div class="page" data-astro-cid-gtkanwr>
<h1 data-astro-cid-gtkanwr>Über uns</h1>
</div>
<style>
.page[data-astro-cid-gtkanwr] { max-width: 640px; }
</style>
i18n
Alle UI-sichtbaren Strings liegen in i18n/ui.ts (de/en), eine Hilfsfunktion useTranslations(lang) gibt die Labels der gewünschten Sprache zurück, mit Fallback auf de.
Content Collections
Typsichere Definition der Inhalte mit Zod-Schema in content.config.ts:
const recipes = defineCollection({
loader: glob({ pattern: "**/*.yaml", base: "./src/content/recipes" }),
schema: z.object({
title: z.object({ de: z.string(), en: z.string() }),
category: z.enum(["Vegetarisch", "Fleisch", "Dessert", "Vegan"]),
cookTime: z.number().positive(),
difficulty: z.number().int().min(1).max(5),
servings: z.number().int().positive(),
featured: z.boolean().default(false),
image: z.string(),
ingredients: z.array(z.object({
name: z.string(), amount: z.number().positive(), unit: z.string(),
})),
steps: z.array(z.string()),
}),
});
Die Rezepte selbst liegen als YAML-Dateien unter src/content/recipes/. Bei Schema-Verstoß (z. B. fehlende en-Übersetzung) schlägt der Build fehl — mit klarer Fehlermeldung samt Feld und Datei.
Vergleich: Bei klassischem JSON ohne Validierung (React-Baseline) fällt derselbe Fehler erst zur Laufzeit auf — als leere Übersetzung oder
undefined, eventuell erst, wenn ein Nutzer sich beschwert.
Abgerufen wird die Collection im Frontmatter per await getCollection('recipes').
Dynamische Generierung von Seiten
---
export async function getStaticPaths() {
const recipes = await getCollection("recipes");
return recipes.map((recipe) => ({
params: { slug: recipe.id },
props: { recipe },
}));
}
const { recipe } = Astro.props;
---
<BaseLayout title={`${recipe.data.title.de} - Rezeptbuch`} …>
<RecipeDetailView lang={lang} recipe={recipe} />
</BaseLayout>
Die exportierte getStaticPaths() gibt die Liste der Seitenkonfigurationen zurück: Pfad-Parameter für den dynamischen Pfad, Props für den Seiteninhalt. Aus 8 YAML-Dateien werden so 8 vorgerenderte HTML-Seiten pro Sprache.
Interaktivität — ohne Framework
1. Kochmodus-Checkliste — das simpelste der vier Features, das Skript steht inline in der View-Komponente. Fünf Zeilen JS reichen:
document
.querySelectorAll('.step-list input[type="checkbox"]')
.forEach((checkbox) => {
checkbox.addEventListener("change", () => {
checkbox.closest("li")?.classList.toggle("done", checkbox.checked);
});
});
Das zeigt den Kern der „Zero JS by default"-Idee am deutlichsten: keine Komponenten-Architektur, kein State-Container — einfach ein Skript dort, wo es gebraucht wird.
2. Portionsrechner — eigene Komponente PortionCalculator.astro mit einem ausgelagerten Skript. data-Attribute sind der Weg, Variablen aus dem Build-Time-Frontmatter in die Runtime-Umgebung zu bekommen:
<div class="portion-calculator" data-base-servings={servings}>
…
<span class="amount" data-amount={ing.amount} data-unit={ing.unit}>
{ing.amount} {ing.unit}
</span>
</div>
<script>
import '../scripts/portion-calculator';
</script>
Die Kernlogik in scripts/portion-calculator.ts skaliert die Mengen und schreibt sie per direkter DOM-Manipulation zurück — kein virtueller DOM, kein Neu-Rendern.
3. Favoriten-Toggle — liest und schreibt direkt in localStorage, kein globales State-Management (Context/Redux) nötig. Jede Button-Instanz synchronisiert unabhängig ihren Zustand über eine sync()-Funktion; mehrere Instanzen auf einer Seite sind völlig unabhängig voneinander.
4. Such- und Filterleiste — funktioniert analog: direkte DOM-Manipulation, um Rezeptkarten ein- bzw. auszublenden. Die Karten selbst werden von der Insel nicht neu gerendert — RecipesView.astro reicht die fertig gerenderten RecipeCards als Kind-Inhalt (Slot) an SearchFilter durch.
Zwischenfazit
Interaktivität geht auch ohne Framework!
- Man kann React/Vue/Svelte/Solid/Preact-Komponenten als Islands einbinden — dann kommt aber direkt wieder ein großes JS-Bundle mit.
- Echte Abwägung im Projekt: Bundle-Größe vs. Team-Vertrautheit/Entwicklungsgeschwindigkeit. Bei komplexeren Features (z. B. Formularen) kann eine React-Insel pragmatischer sein.
- Der Punkt ist nicht „Vanilla ist immer richtig", sondern: bewusste Abwägung statt Reflex.
Benchmark-Vergleich
| Metrik | recipe-react (SPA) | recipe-astro |
|---|---|---|
Build-Output gesamt (dist/) |
284,7 KB | 260,6 KB |
| LCP (Rezepteübersicht) | 146 ms | 93 ms |
| übertragene Datenmenge (Rezepteübersicht) | 89,8 KB | 5,9 KB |
| LCP („Über uns") | 68 ms | 38 ms |
| übertragene Datenmenge („Über uns") | 89,8 KB | 3,5 KB |
- Der Build ist trotz mehrerer vorgerenderter Seiten (pro Sprache/Route, ø ≈ 11,8 KB pro Seite) nicht größer, sondern sogar kleiner als das SPA-Bundle — weil kein Framework-Runtime mehrfach eingebettet wird.
- Die Messungen liefen lokal ohne echte Netzwerklatenz; mit echter Latenz bleibt die relative Differenz bestehen bzw. wird größer.
- Auf der bilderfreien Über-uns-Seite ist der Unterschied am deutlichsten: Faktor ≈ 26× bei der Übertragungsgröße. Ursache ist u. a., dass die SPA dort
recipes.json(14 KB) mitlädt, obwohl die Seite keine Rezeptdaten braucht —RecipesContextlädt die Liste global beim Mounten der App. - Umgekehrt gilt: Auf bildlastigen Seiten verschwindet der Architekturunterschied fast in der gemeinsamen Bildlast (≈ 1 % der Gesamtübertragung). Je textlastiger und bilderärmer eine Seite ist, desto stärker wirkt sich der SPA-Bundle-Overhead relativ aus. (Details und Messmethodik:
COMPARISON.mdim Repo.)
06. Vom Client zum Server
„Zero JS by default" heißt nicht Verzicht auf dynamische Anforderungen. Astro kann mehr als nur statisch: SSR, Server Islands, Actions, Endpoints.
Serveradapter
Konzeptionell ist es der Umstieg von output: 'static' auf output: 'server' — aber kein reiner Konfigurationsschalter: Es ist zwingend ein Deployment-Adapter nötig (offiziell für Node, Vercel, Cloudflare, Netlify).
export default defineConfig({
output: "server",
adapter: node({ mode: "standalone" }),
i18n: { defaultLocale: "de", locales: ["de", "en"], … },
});
mode: 'standalone' startet einen eigenständigen Node-Prozess, statt sich als Middleware einzuklinken. Folge: reines statisches Hosting reicht nicht mehr — eine laufende Serverinstanz oder Edge-Function ist nötig, mit Implikationen für Pipeline, Kosten und Skalierung.
Wichtig: output: 'server' heißt nicht „jetzt ist alles dynamisch". Pro Seite ist das explizit wählbar:
---
export const prerender = true;
---
Im Beispielprojekt bleiben Rezept-Detailseiten, Übersicht, Startseite und Über-uns komplett vorgerendert — nur die wirklich dynamischen Teile werden on-demand gerendert.
Actions
Typisierte, serverseitige Funktionen, die sich wie normale JS-Funktionen aufrufen lassen. Alternative zu einem rohen POST-Endpoint: Input-Validierung (Zod) und Server-Handler stecken in einer typisierten Funktion.
export const server = {
rateRecipe: defineAction({
accept: "form",
input: z.object({
slug: z.string(),
rating: z.coerce.number().int().min(1).max(5),
}),
handler: async ({ slug, rating }) => {
addRating(slug, rating);
return getRating(slug);
},
}),
};
accept: 'form' erlaubt ein klassisches Formular (FormData) als Input; der Handler läuft ausschließlich auf dem Server, der Rückgabewert kommt typisiert beim Aufrufer an:
const { data, error } = await actions.rateRecipe(new FormData(form));
Server Islands
Dynamische, on-demand gerenderte Inseln. server:defer rendert eine Komponente weiterhin serverseitig, entkoppelt aber ihre Auslieferung vom Rest der Seite:
<ServerHighlights server:defer lang={lang} currentSlug={recipe.id}>
<p slot="fallback">{t("server.loading")}</p>
</ServerHighlights>
Astro rendert einen Platzhalter ins statische HTML; die eigentliche Komponente wird per separatem Request nachgeladen. Dadurch bleiben TTFB und LCP der Hauptseite unabhängig davon, wie lange die Insel für ihre Berechnung braucht.
Im Beispiel: „gerade beliebte Rezepte" samt Aufrufzähler und Durchschnittsbewertung — zu häufig wechselnd für Build-Zeit-Rendering. Das Frontmatter der Insel läuft zur Laufzeit statt zur Build-Zeit, sie holt sich ihre Daten selbst (eigener getCollection-Aufruf) und ist damit eine eigenständige, an den Request gebundene Einheit.
SSR
Die komplette Seite wird on-demand gerendert (im Beispiel: die Statistik-Seite mit export const prerender = false) — sinnvoll für Inhalte, die sich zu häufig ändern oder zu individuell sind, um vorab gebaut zu werden: Dashboards, personalisierte Seiten, Live-Daten.
„Herzlichen Glückwunsch, wir haben PHP neu erfunden." Keine reine Wiederholung — Islands sind gezielter und granularer als klassisches PHP-Rendering. Aber die Grundrichtung „zurück zum Server" stimmt.
Use Cases für Server-Funktionen
- Authentifizierung & personalisierte Inhalte
- CAPTCHAs (müssen serverseitig validiert werden)
- Häufig wechselnde, nicht vorab bekannte Daten (Dashboard, Warenkorb, Live-Preise)
Botschaft: gezielt dynamisch nur für die Teile, die es brauchen — nicht die ganze App. Vorteil gegenüber „alles ist SPA": inkrementell genau so viel Dynamik hinzufügen wie nötig, das hält den ausgelieferten Code minimal.
07. Fazit
Wann SPAs die bessere Wahl sind
→ Komplexe, hochinteraktive Anwendungen mit viel Client-State z. B. Dashboards, Editoren, Tools mit dauerhaftem „App-Charakter"
Das ist keine Ausnahme von der Kernthese, sondern ihre logische Konsequenz: Bei interaktiven Tools ist „sehr viel JS" die ehrliche Antwort — dann ist die SPA konsequent richtig und keine schlechte Gewohnheit. Komplexe JS-Logik ist mit einem Framework meist einfacher zu entwickeln als in reinem JS/TS.
Ein Fragenkatalog für euer nächstes Projekt
- Wie viel der Seite ist tatsächlich interaktiv vs. nur reiner Content? Marketing-Seite, Blog, Doku, Rezepte-App = überwiegend Content → Islands-/Content-first-Ansatz. Grafik-Editor, PM-Board, Dashboard = fast alles interaktiv → klassische SPA.
- Wird clientseitiger Zustand benötigt, der über viele Komponenten hinweg geteilt und synchron gehalten werden muss? Islands sind bewusst isoliert — Stärke bei einfachen lokalen Interaktionen, Schwäche bei viel geteiltem State (Warenkorb-Icon, Formular mit Cross-Feld-Validierung). Lösbar auch mit Islands (Astro empfiehlt Nanostores), aber ab einem gewissen Komplexitätsgrad ist eine SPA mit eingebautem State-Management oft pragmatischer.
- Ist SEO bzw. sind die Core Web Vitals ein kritischer Faktor? Öffentliche Inhalte (Marketing, Blog, Produktseiten, Doku): fast immer „ja". Interne Tools, Admin, login-geschützt: SEO irrelevant, andere Kriterien entscheiden.
- Besteht bereits eine erhebliche Investition in ein bestehendes SPA-Setup? Team-Know-how, gewachsene Codebasis und Tooling-Integrationen haben realen Wert und reale Umstellungskosten. Ein technisch „besseres" Paradigma rechtfertigt nicht automatisch eine Neuentwicklung. Junges Projekt + starke Antworten auf Fragen 1–3 → Architektur-Vorteil überwiegt; etabliertes Produkt mit eingespieltem Team → oft anders.
Der Fragenkatalog ist bewusst tool-unabhängig formuliert. Die wichtigste Botschaft: nicht ein Tool gegen ein anderes tauschen, sondern den Entscheidungsprozess selbst bewusster gestalten.
TL;DR
Die entscheidende Frage ist nicht „Welches Framework?", sondern „Wie viel JavaScript braucht diese Seite tatsächlich?"
- Die zentrale Aussage ist nicht „nutzt Astro" — es gibt andere Frameworks mit ähnlichen Ansätzen (Qwik, htmx, Next.js RSC).
- Call to Action: Das nächste Projekt anhand des Fragenkatalogs einordnen. Oder: den Islands-Ansatz an einer kleinen, isolierten Stelle ausprobieren (z. B. eine wenig interaktive Unterseite ohne volles Framework-Bundle).
- Es geht nicht darum, auf jeden Technologie-Trend aufzuspringen, sondern die verschiedenen Ansätze zur Problemlösung zu verstehen — und zu wissen, wann sie eingesetzt werden können und sollten. Egal ob man sie dann weiterverfolgt oder verwirft: sie zu verstehen macht einen zu einem besseren Entwickler.
Links
| Thema | Link |
|---|---|
| Slides & Code (Monorepo) | https://github.com/VascoSchwarze/do-you-really-need-an-spa |
| Benchmark-Details & Messmethodik | COMPARISON.md im Repo |
| Beispiel: React-SPA | examples/recipe-react |
| Beispiel: Astro (SSG) | examples/recipe-astro |
| Beispiel: Astro mit Node-Adapter (SSR, Server Islands, Actions) | examples/recipe-astro-server |
| Astro | https://astro.build |
| „JS-Bloat in 2024", Nikita Prokopov | https://tonsky.me/blog/js-bloat/ |
| „Second-guessing the modern web", Tom MacWright | https://macwright.com/2020/05/10/spa-fatigue |
| Just Fucking Use HTML | https://justfuckingusehtml.com |
| Sprecher | vasco.schwarze@sciota.io · GitHub: VascoSchwarze |