tanstack-start-vs-frameworks Svelte Themes

Tanstack Start Vs Frameworks

TanStack Start vs Next.js, Remix, Svelte, and Astro: A Client-Side Deep Dive (2026)

TanStack Start vs Next.js, Remix, Svelte, and Astro: A Client-Side Deep Dive (2026)

The React meta-framework landscape fractured in 2025. Remix merged into React Router v7, then announced v3 would drop React entirely. Next.js doubled down on server components with v16. TanStack Start shipped its first stable release, betting that client-first was the right default. Cloudflare acquired Astro. SvelteKit quietly kept winning developer satisfaction surveys.

If you are choosing a framework for a new project in 2026, the decision is harder than ever -- and the stakes are higher. Your framework choice determines bundle size, type safety, deployment flexibility, hiring pool, and how much JavaScript your users download before they can click a button.

This article compares TanStack Start, Next.js, SvelteKit, Astro, and the Remix/React Router lineage through a client-side lens: what ships to the browser, how interactivity works, and what your users actually experience.

What You Will Learn

  • How TanStack Start's server functions compare to Next.js Server Actions and SvelteKit form actions
  • Real bundle size differences for a dashboard-class application
  • Which framework wins on Core Web Vitals for different use cases
  • The Remix situation: what happened, where its users went, and what React Router v7 actually is
  • Data fetching, caching, and state management patterns across all five frameworks
  • Middleware and authentication patterns that affect client-side architecture
  • A decision framework for choosing between these options based on your constraints
  • Anti-patterns that waste bundle budget regardless of framework

Table of Contents


The Problem: Why Client-Side Considerations Matter

Server-side rendering gets the headlines. Edge functions get the conference talks. But your users experience the client side: the JavaScript that downloads, parses, executes, and ultimately determines whether your app feels fast or sluggish.

Three metrics define client-side quality:

  1. Bundle size -- how much JavaScript ships to the browser
  2. Time to Interactive (TTI) -- when users can actually click things
  3. Runtime performance -- how the framework handles updates after hydration

These metrics compound. A 100KB bundle difference means 200-400ms on a median mobile connection. That gap widens on repeat visits if your caching strategy is wrong, and it multiplies across every route transition if your code-splitting is not set up correctly.

Key insight: The best server-side rendering in the world cannot compensate for shipping 300KB of client JavaScript that blocks interactivity. Framework choice is a bundle size decision first and a DX decision second.

Here is the uncomfortable truth: most framework comparisons focus on developer experience, deployment options, and feature lists. Those matter for the team building the app. But the user never sees your DX. They see load time, responsiveness, and whether the button they tapped actually did something.

The JavaScript Cost Model

Not all kilobytes are equal. JavaScript is more expensive than images or CSS because the browser must download, parse, compile, and execute it -- all on the main thread. Here is how the cost breaks down:

Phase What Happens Affected By
Download Bytes over the network Bundle size, compression, CDN
Parse Browser reads and tokenizes the JS Code complexity, module count
Compile JIT compilation to machine code Code patterns, eval usage
Execute Framework initialization, hydration Framework runtime size, component tree depth

A study by Google's Web Performance team found that on a median mobile device, each kilobyte of JavaScript costs approximately 2ms of main-thread time. That means a 100KB framework runtime consumes 200ms before a single line of your application code runs.

What Changes If You Get This Right

Metric Poor Framework Choice Good Framework Choice
Initial JS payload 200-400KB gzipped 50-120KB gzipped
LCP 2.5-4.0s 0.8-1.8s
INP (Interaction to Next Paint) 200-500ms 50-150ms
Route transition 300-800ms 50-200ms
Lighthouse Performance 45-65 85-100

The difference between a good and poor choice is not theoretical. It is the difference between passing and failing Core Web Vitals, which directly affects search ranking.

The Hydration Tax

Every server-rendered framework pays a hydration cost. The server sends HTML, then the client downloads JavaScript to make that HTML interactive. During hydration, the framework walks the DOM tree, attaches event handlers, and reconciles its internal state with the rendered HTML.

This cost varies dramatically by framework:

  • Astro: Zero hydration for static components. Islands hydrate independently.
  • SvelteKit: Minimal hydration (~2KB runtime, surgical DOM binding).
  • TanStack Start: React hydration (~44KB runtime, VDOM reconciliation).
  • Next.js: React hydration + RSC protocol deserialization (~60KB+ runtime).

The hydration gap is why a SvelteKit page feels interactive almost instantly while a Next.js page with the same content has a visible "dead zone" where buttons render but do not respond.


TanStack Start: The New Contender

TanStack Start is a full-stack React framework built on TanStack Router, Vite, and Nitro. It takes a fundamentally different position than Next.js: client-first by default, with server capabilities available when you need them.

Where Next.js starts from the server and requires "use client" to opt into client-side behavior, TanStack Start starts from the client and uses createServerFn to opt into server behavior. This inversion matters more than it sounds.

Server Functions as an RPC Layer

The core primitive is createServerFn, which creates a type-safe function that runs on the server but can be called from client code as if it were a local function:

// server/db.ts
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
import { db } from './database'

const GetTeamSchema = z.object({
  teamId: z.string().uuid(),
  includeMembers: z.boolean().default(false),
})

export const getTeam = createServerFn()
  .inputValidator(GetTeamSchema)
  .handler(async ({ data }) => {
    const team = await db.query.teams.findFirst({
      where: (teams, { eq }) => eq(teams.id, data.teamId),
      with: data.includeMembers ? { members: true } : undefined,
    })

    if (!team) {
      throw new Error('Team not found')
    }

    return team
  })

export const updateTeamName = createServerFn({ method: 'POST' })
  .inputValidator(z.object({
    teamId: z.string().uuid(),
    name: z.string().min(1).max(100),
  }))
  .handler(async ({ data }) => {
    const updated = await db
      .update(teams)
      .set({ name: data.name, updatedAt: new Date() })
      .where(eq(teams.id, data.teamId))
      .returning()

    return updated[0]
  })

On the client, you call these functions directly. No fetch calls, no API route definitions, no manual type synchronization:

// routes/teams/$teamId.tsx
import { createFileRoute } from '@tanstack/react-router'
import { getTeam, updateTeamName } from '../../server/db'

export const Route = createFileRoute('/teams/$teamId')({
  loader: ({ params }) =>
    getTeam({ data: { teamId: params.teamId, includeMembers: true } }),
  component: TeamPage,
})

function TeamPage() {
  const team = Route.useLoaderData()
  const [name, setName] = useState(team.name)

  const handleSave = async () => {
    await updateTeamName({ data: { teamId: team.id, name } })
  }

  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <button onClick={handleSave}>Save</button>
      {team.members?.map((m) => (
        <div key={m.id}>{m.email} — {m.role}</div>
      ))}
    </div>
  )
}

Key insight: createServerFn with .inputValidator() gives you runtime validation AND compile-time types from a single Zod schema. No tRPC server needed, no API route boilerplate, no manual fetch calls. The network boundary becomes invisible.

Type-Safe Routing

TanStack Router provides the strongest type safety of any React router. Route parameters, search parameters, loader data, and navigation are all fully typed:

// routes/dashboard/analytics.tsx
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'

const AnalyticsSearchSchema = z.object({
  dateRange: z.enum(['7d', '30d', '90d']).default('30d'),
  metric: z.enum(['pageviews', 'visitors', 'conversions']).optional(),
  brandId: z.string().uuid().optional(),
})

export const Route = createFileRoute('/dashboard/analytics')({
  validateSearch: AnalyticsSearchSchema,
  loaderDeps: ({ search }) => ({ search }),
  loader: async ({ deps: { search }, context: { queryClient } }) => {
    await queryClient.ensureQueryData(
      analyticsQueryOptions(search.dateRange, search.metric, search.brandId)
    )
  },
  component: AnalyticsDashboard,
})

function AnalyticsDashboard() {
  const { dateRange, metric, brandId } = Route.useSearch()
  //    ^? { dateRange: '7d' | '30d' | '90d'; metric?: 'pageviews' | ... ; brandId?: string }
  const navigate = Route.useNavigate()

  return (
    <div>
      <select
        value={dateRange}
        onChange={(e) =>
          navigate({
            search: (prev) => ({ ...prev, dateRange: e.target.value as any }),
          })
        }
      >
        <option value="7d">Last 7 Days</option>
        <option value="30d">Last 30 Days</option>
        <option value="90d">Last 90 Days</option>
      </select>
      {/* Dashboard content */}
    </div>
  )
}

Every navigate() call, every <Link> component, every useSearch() hook is checked against the route tree at compile time. If you rename a route parameter, TypeScript catches every broken reference.

Bundle Size Strategy

TanStack Start uses Vite for bundling, which means tree-shaking works correctly out of the box. Server functions are automatically stripped from client bundles -- the client only gets a thin RPC stub that makes an HTTP request.

Practical bundle sizes for a TanStack Start dashboard app:

Component Gzipped Size
TanStack Router runtime ~12KB
TanStack Query runtime ~13KB
React + ReactDOM ~44KB
App code (10 routes) ~25-40KB
Total initial load ~90-110KB

The React runtime is the largest piece. This is an inherent cost of being a React framework -- TanStack Start cannot escape it, but it adds minimal overhead on top.

Deployment Flexibility

TanStack Start deploys anywhere Nitro does: Cloudflare Workers, Vercel, Netlify, Node.js, Deno, Bun, AWS Lambda. The cloudflare-module preset generates a Worker-compatible build:

// app.config.ts
import { defineConfig } from '@tanstack/react-start/config'

export default defineConfig({
  server: {
    preset: 'cloudflare-module',
  },
})

Deploy with a single command:

wrangler deploy

Key insight: TanStack Start is the only React framework with first-class Cloudflare Workers support that does not require vendor-specific adapters or middleware hacks. If you deploy to Cloudflare, this is the path of least resistance.

Best For

  • SaaS dashboards with complex client state
  • Applications deploying to Cloudflare Workers or multiple edge platforms
  • Teams already using TanStack Query and wanting deeper integration
  • Projects where type safety across the full stack is a priority

Limitations

  • No React Server Components support yet (in development)
  • Smaller ecosystem than Next.js (fewer templates, tutorials, third-party integrations)
  • Younger framework -- edge cases and undocumented behaviors exist
  • Still requires the full React runtime (~44KB gzipped)

Next.js: The Industry Standard

Next.js needs no introduction. It is the most widely deployed React framework, backed by Vercel, and the default choice for most teams starting a React project. Version 16 (October 2025) made Turbopack the default bundler and stabilized Partial Prerendering.

But "most popular" and "best for your use case" are different claims. Let us examine what Next.js actually ships to the browser.

The App Router Client Architecture

Next.js App Router defaults to Server Components. Components are server-rendered unless you add "use client" at the top of the file. This sounds like it should produce smaller bundles -- and for content-heavy pages, it does.

The problem appears when you build interactive applications. A SaaS dashboard is mostly "use client" components. At that point, you are shipping React, ReactDOM, the Next.js router runtime, AND the RSC protocol overhead for hydrating the server-rendered shell:

// app/dashboard/page.tsx (Server Component - minimal JS)
import { DashboardShell } from './dashboard-shell'

export default async function DashboardPage() {
  const data = await fetch('https://api.example.com/metrics')
  const metrics = await data.json()
  return <DashboardShell initialMetrics={metrics} />
}
// app/dashboard/dashboard-shell.tsx
'use client'

import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'

export function DashboardShell({ initialMetrics }: { initialMetrics: Metrics }) {
  const [metrics, setMetrics] = useState(initialMetrics)
  const [dateRange, setDateRange] = useState('30d')
  const router = useRouter()

  useEffect(() => {
    const interval = setInterval(async () => {
      const res = await fetch(`/api/metrics?range=${dateRange}`)
      setMetrics(await res.json())
    }, 30000)
    return () => clearInterval(interval)
  }, [dateRange])

  return (
    <div>
      <select
        value={dateRange}
        onChange={(e) => setDateRange(e.target.value)}
      >
        <option value="7d">7 Days</option>
        <option value="30d">30 Days</option>
        <option value="90d">90 Days</option>
      </select>
      <MetricsGrid data={metrics} />
      <ChartPanel data={metrics} />
    </div>
  )
}
// app/api/metrics/route.ts
import { NextResponse } from 'next/server'
import { db } from '@/lib/db'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const range = searchParams.get('range') || '30d'
  const metrics = await db.getMetrics(range)
  return NextResponse.json(metrics)
}

Notice the pattern: Server Component fetches initial data, passes it to a Client Component, and then the Client Component re-fetches via an API route for updates. You end up writing the data fetching logic twice and maintaining a separate API route.

Image and Font Optimization

Where Next.js genuinely excels on the client side is asset optimization. next/image handles responsive images, lazy loading, format conversion (WebP/AVIF), and blur placeholders automatically:

import Image from 'next/image'

export function HeroSection() {
  return (
    <Image
      src="/hero.jpg"
      alt="Product screenshot"
      width={1200}
      height={800}
      priority // Preloads for LCP
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,..."
    />
  )
}

next/font eliminates layout shift from font loading by self-hosting fonts and generating optimal @font-face declarations:

import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'] })

export default function RootLayout({ children }) {
  return (
    <html className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

These built-in optimizations are difficult to replicate manually and provide measurable LCP improvements, particularly for marketing sites and content platforms.

Bundle Size Reality

Next.js 16 with Turbopack has improved significantly, but the framework runtime is not small:

Component Gzipped Size
React + ReactDOM ~44KB
Next.js router + runtime ~30-50KB
RSC protocol layer ~8-15KB
App code (10 routes) ~25-40KB
Total initial load ~110-150KB

The RSC protocol adds overhead that TanStack Start does not have. For content-heavy sites where most components are Server Components, this overhead is justified by the reduction in component JavaScript. For interactive dashboards, it is pure overhead.

Vercel Lock-In

Next.js works on other platforms, but it works best on Vercel. Features like ISR, Image Optimization, edge middleware, and analytics are either Vercel-only or require extra configuration elsewhere.

Deploying Next.js to Cloudflare Workers is possible via OpenNext but requires significant configuration and does not support all features. This is not a theoretical concern -- it is a deployment constraint that should factor into your decision.

Best For

  • Marketing sites and content platforms where image optimization matters
  • Teams already invested in the Vercel ecosystem
  • Projects where hiring is a constraint (largest talent pool)
  • E-commerce with heavy SSG/ISR requirements

Limitations

  • Heaviest client-side runtime of the React frameworks compared here
  • Vercel optimization creates vendor dependency
  • App Router complexity (Server/Client Component boundary rules)
  • API routes require separate type definitions (no automatic RPC)
  • Turbopack improved DX but did not reduce shipped bundle size

Remix / React Router v7: The Progressive Enhancement Champion

The Remix story is complicated in 2026, so let us untangle it.

Timeline:

  1. Remix v2 was a full-stack React framework with a focus on web standards
  2. In November 2024, Remix merged into React Router v7 -- the framework features (loaders, actions, SSR) became part of React Router
  3. In May 2025, the Remix team announced v3 would drop React entirely in favor of a Preact fork
  4. React Router v7 continues as the spiritual successor to Remix for React developers

This means if you were a Remix user, your migration path is React Router v7 (for React) or TanStack Start (for a more opinionated framework experience). Many chose TanStack Start.

What React Router v7 Offers

React Router v7 with framework mode gives you the Remix experience -- file-based routing, loaders, actions, and form handling -- inside React Router:

// routes/contacts/$contactId.tsx
import type { Route } from './+types/contacts.$contactId'
import { Form, useLoaderData } from 'react-router'

export async function loader({ params }: Route.LoaderArgs) {
  const contact = await db.contacts.findUnique({
    where: { id: params.contactId },
  })
  if (!contact) throw new Response('Not Found', { status: 404 })
  return { contact }
}

export async function action({ request, params }: Route.ActionArgs) {
  const formData = await request.formData()
  const name = formData.get('name') as string
  await db.contacts.update({
    where: { id: params.contactId },
    data: { name },
  })
  return { ok: true }
}

export default function Contact() {
  const { contact } = useLoaderData<typeof loader>()
  return (
    <Form method="post">
      <input name="name" defaultValue={contact.name} />
      <button type="submit">Save</button>
    </Form>
  )
}

Progressive Enhancement: The Killer Feature

The single most compelling client-side feature of the Remix/React Router lineage is progressive enhancement. Forms work without JavaScript:

// This form submits and works even if JS fails to load
export default function SearchPage() {
  return (
    <Form method="get" action="/search">
      <input type="text" name="q" placeholder="Search..." />
      <button type="submit">Search</button>
    </Form>
  )
}

When JavaScript loads, the same form is enhanced with client-side navigation, optimistic UI, and pending states. This is not achievable in TanStack Start or Next.js App Router -- both require JavaScript for navigation.

Key insight: If your application must work on unreliable networks, in low-bandwidth regions, or for users who disable JavaScript, the Remix/React Router progressive enhancement model is the only viable option in the React ecosystem.

The Remix v3 Situation

Remix v3 is dropping React for a Preact fork with no migration path from v2. This is a deliberate break. The team wants to own their rendering layer and optimize for LLM-friendly code generation and web standards.

For React developers, this means Remix v3 is a different framework entirely. Do not plan a migration to it if you want to stay in the React ecosystem.

Best For

  • Applications requiring progressive enhancement (government, accessibility-critical)
  • Content-heavy sites with form submissions
  • Teams that value web standards over framework abstractions

Limitations

  • React Router v7 framework mode is less opinionated than Remix was
  • Community split between React Router v7, TanStack Start, and Remix v3
  • Smaller ecosystem than Next.js
  • Type safety is weaker than TanStack Router (no validated search params)

SvelteKit: The Lean Alternative

SvelteKit is not a React framework. Including it here is deliberate: if your primary concern is client-side performance, you should seriously consider leaving React behind.

Svelte compiles your components to vanilla JavaScript at build time. There is no virtual DOM, no runtime diffing, no framework code shipped to the browser. The result is dramatically smaller bundles and faster runtime performance.

True Reactivity Without a Runtime

React re-renders entire component trees and diffs a virtual DOM. Svelte generates surgical DOM updates at compile time:

<!-- src/routes/dashboard/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types'

  export let data: PageData

  let dateRange = '30d'
  let metrics = data.metrics

  // Reactive declaration: re-runs when dateRange changes
  $: filteredMetrics = metrics.filter((m) => m.range === dateRange)
  $: totalRevenue = filteredMetrics.reduce((sum, m) => sum + m.revenue, 0)

  async function refreshMetrics() {
    const res = await fetch(`/api/metrics?range=${dateRange}`)
    metrics = await res.json()
  }
</script>

<select bind:value={dateRange} on:change={refreshMetrics}>
  <option value="7d">7 Days</option>
  <option value="30d">30 Days</option>
  <option value="90d">90 Days</option>
</select>

<p>Total Revenue: ${totalRevenue.toLocaleString()}</p>

{#each filteredMetrics as metric}
  <div class="metric-card">
    <h3>{metric.name}</h3>
    <span>{metric.value}</span>
  </div>
{/each}
// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from './$types'
import { db } from '$lib/server/db'

export const load: PageServerLoad = async () => {
  const metrics = await db.getMetrics('30d')
  return { metrics }
}

The compiled output for this component is roughly 2-3KB. An equivalent React component with useState, useEffect, and useMemo would be larger in application code AND require the 44KB React runtime.

Bundle Size: The Numbers

SvelteKit bundle sizes for a dashboard application:

Component Gzipped Size
Svelte runtime ~2KB
SvelteKit router ~5KB
App code (10 routes) ~15-25KB
Total initial load ~22-32KB

This is 3-5x smaller than any React framework. The difference is not marginal -- it is a category difference.

Svelte 5 Runes

Svelte 5 introduced runes, which replace the $: reactive declarations with a more explicit API:

<script lang="ts">
  let count = $state(0)
  let doubled = $derived(count * 2)

  function increment() {
    count++  // Direct mutation, Svelte tracks it
  }
</script>

<button onclick={increment}>
  {count} x 2 = {doubled}
</button>

Runes make Svelte's reactivity more predictable and closer to signals-based reactivity (like SolidJS). The compiled output is still minimal.

Form Actions

SvelteKit has its own progressive enhancement story through form actions:

<!-- src/routes/settings/+page.svelte -->
<script lang="ts">
  import { enhance } from '$app/forms'
  import type { ActionData } from './$types'

  export let form: ActionData
</script>

<form method="POST" action="?/updateProfile" use:enhance>
  <input name="name" value={form?.name ?? ''} />
  {#if form?.error}
    <p class="error">{form.error}</p>
  {/if}
  <button type="submit">Save</button>
</form>
// src/routes/settings/+page.server.ts
import type { Actions } from './$types'
import { fail } from '@sveltejs/kit'

export const actions: Actions = {
  updateProfile: async ({ request }) => {
    const data = await request.formData()
    const name = data.get('name') as string

    if (name.length < 2) {
      return fail(400, { name, error: 'Name must be at least 2 characters' })
    }

    await db.updateProfile({ name })
    return { success: true }
  },
}

Ecosystem Limitations

The trade-off for Svelte's performance is ecosystem size. As of March 2026:

Metric React Svelte
npm weekly downloads ~28M ~1.5M
Component libraries 100+ mature ~15 mature
Job postings (US) ~45,000 ~3,000
Stack Overflow questions ~450K ~25K
UI kits (production-ready) MUI, Radix, Shadcn, Mantine, Chakra Skeleton, Melt, Bits UI

If you need a specific integration -- a rich text editor, a data grid, a charting library -- it probably exists for React and may not exist for Svelte.

Best For

  • Performance-critical applications where bundle size matters most
  • Interactive sites with animations and transitions
  • Teams willing to invest in a smaller ecosystem for better performance
  • Projects where you control the full stack and do not need React-specific libraries

Limitations

  • Much smaller ecosystem and hiring pool than React
  • Learning a new language (not just a framework)
  • Some React libraries have no Svelte equivalent
  • TypeScript support is good but not as deeply integrated as TanStack Router

Astro: The Content-First Framework

Astro takes the most radical approach: ship zero JavaScript by default. Every component renders to static HTML unless you explicitly opt into client-side interactivity with a directive.

Since Cloudflare acquired Astro in January 2026, the framework has strong infrastructure backing and is tightly integrated with the Cloudflare platform.

Island Architecture

Astro's island architecture lets you embed interactive components from any framework (React, Svelte, Vue, SolidJS) into otherwise static pages:

---
// src/pages/pricing.astro
import Header from '../components/Header.astro'
import PricingTable from '../components/PricingTable.astro'
import PricingCalculator from '../components/PricingCalculator.tsx'
import Testimonials from '../components/Testimonials.svelte'
import Footer from '../components/Footer.astro'
---

<html>
  <body>
    <!-- Static: 0 JS -->
    <Header />

    <!-- Static: 0 JS -->
    <PricingTable />

    <!-- Interactive island: React component, hydrates when visible -->
    <PricingCalculator client:visible />

    <!-- Interactive island: Svelte component, hydrates on page load -->
    <Testimonials client:load />

    <!-- Static: 0 JS -->
    <Footer />
  </body>
</html>

The client:* directives control when islands hydrate:

Directive Behavior Use Case
client:load Hydrate immediately on page load Critical interactive elements
client:idle Hydrate when browser is idle Non-critical widgets
client:visible Hydrate when scrolled into view Below-the-fold content
client:media Hydrate when media query matches Responsive components
client:only Skip SSR, render only on client Browser-only APIs

Zero JavaScript by Default

An Astro page with no client: directives ships zero JavaScript:

---
// src/pages/blog/[slug].astro
import { getEntry } from 'astro:content'
import BlogLayout from '../../layouts/BlogLayout.astro'

const { slug } = Astro.params
const post = await getEntry('blog', slug)
const { Content } = await post.render()
---

<BlogLayout title={post.data.title}>
  <article>
    <h1>{post.data.title}</h1>
    <time>{post.data.date.toLocaleDateString()}</time>
    <Content />
  </article>
</BlogLayout>

This page is pure HTML and CSS. No React runtime, no router, no hydration. The browser receives exactly what it needs and nothing more.

Bundle Size

Astro's bundle strategy depends entirely on how many islands you use:

Scenario JS Shipped
Blog post (no islands) 0KB
Marketing page (1 React island) ~50KB (React runtime + component)
Marketing page (1 Svelte island) ~5KB (Svelte component, no runtime)
Dashboard (many React islands) ~100-150KB (approaches SPA territory)

Key insight: Astro's advantage disappears when most of your page is interactive. If you need a full dashboard, you are effectively building a React SPA inside Astro with extra hydration overhead. Use Astro for content-heavy sites with isolated interactivity, not for applications.

When Astro Struggles

Astro is not designed for applications with heavy client-side state. If you find yourself adding client:load to most components, you have the wrong framework. Specific pain points:

  • Shared state between islands requires external stores (nanostores, etc.)
  • Client-side navigation is available via View Transitions but less capable than a proper SPA router
  • Real-time updates require manual WebSocket/SSE integration per island
  • Complex forms spanning multiple islands are awkward

Best For

  • Documentation sites (Starlight is excellent)
  • Blogs and content-heavy marketing sites
  • Landing pages and portfolio sites
  • Sites where SEO and load speed are the primary metrics

Limitations

  • Not suitable for application-class interactivity
  • Islands cannot easily share state
  • Client-side navigation is limited compared to SPA routers
  • Heavy island usage negates the zero-JS advantage

Head-to-Head Comparison

Feature Comparison

Feature TanStack Start Next.js 16 React Router v7 SvelteKit Astro 6
Language React/TSX React/TSX React/TSX Svelte/TS Any (Astro/TSX)
Bundler Vite Turbopack Vite Vite Vite
SSR Yes Yes Yes Yes Yes
SSG Yes Yes Yes Yes Yes (default)
Streaming SSR Yes Yes Yes Yes Yes
RSC Support No (planned) Yes (core) Experimental N/A N/A
Server Functions createServerFn (RPC) Server Actions Actions/Loaders Form Actions API Routes
Type-Safe Routing Full (params, search, loader) Partial Partial Partial No
Progressive Enhancement No No Yes Yes (use:enhance) N/A (static)
Image Optimization Manual Built-in (next/image) Manual Manual (@sveltejs/enhanced-img) Built-in (astro:assets)
Partial Prerendering No Yes (PPR) No No Yes (islands)
Client-Side Cache TanStack Query (built-in) fetch cache Manual Manual N/A
Code Splitting Route-based (automatic) Route-based (automatic) Route-based Route-based Per-island

Deployment Target Support

Platform TanStack Start Next.js 16 React Router v7 SvelteKit Astro 6
Cloudflare Workers Native Via OpenNext Native Via adapter Native (acquired)
Vercel Yes Native Yes Yes Yes
Netlify Yes Yes Yes Yes Yes
Node.js Yes Yes Yes Yes Yes
Deno Yes Community Yes Yes Yes
AWS Lambda Yes Yes Yes Yes Yes
Static hosting Yes (SSG) Yes (SSG) Yes (SSG) Yes (SSG) Yes (default)
Docker Yes Yes Yes Yes Yes

Developer Experience

Aspect TanStack Start Next.js 16 React Router v7 SvelteKit Astro 6
Dev server startup Fast (Vite) Fast (Turbopack) Fast (Vite) Fast (Vite) Fast (Vite)
HMR speed <100ms <100ms (16.2) <100ms <100ms <100ms
Learning curve Medium High (RSC model) Low-Medium Medium (new language) Low
Tutorials/guides Growing Extensive Moderate Good Good
Error messages Good Improving Good Excellent Good
Community size Small but growing Largest Medium Medium Medium

Bundle Size Analysis

To make this comparison concrete, here are bundle sizes for a realistic dashboard application with 10 routes, a data table, charts, and form inputs. Sizes are gzipped, measured at the initial page load (not total application size).

Initial Page Load (Dashboard Route)

Framework Framework Runtime Router App Code Total
TanStack Start 44KB (React) 12KB 30KB ~86KB
Next.js 16 44KB (React) + 15KB (RSC) 35KB 30KB ~124KB
React Router v7 44KB (React) 18KB 30KB ~92KB
SvelteKit 2KB (Svelte) 5KB 18KB ~25KB
Astro (React islands) 44KB (React) 0KB 30KB ~74KB*

*Astro does not include a client-side router, so navigation triggers full page loads unless using View Transitions.

What These Numbers Mean

                     Initial JS Payload (gzipped KB)
SvelteKit    ████████ 25KB
Astro*       ████████████████████████ 74KB
TanStack     ██████████████████████████████ 86KB
RR v7        ████████████████████████████████ 92KB
Next.js      ████████████████████████████████████████████ 124KB
             0    20   40    60    80   100   120   140

On a 3G connection (400KB/s effective throughput after overhead):

Framework Download Time Parse + Execute Total TTI Penalty
SvelteKit ~60ms ~30ms ~90ms
Astro ~185ms ~80ms ~265ms
TanStack Start ~215ms ~100ms ~315ms
React Router v7 ~230ms ~110ms ~340ms
Next.js 16 ~310ms ~140ms ~450ms

Key insight: The gap between SvelteKit and React-based frameworks is structural. React's runtime (~44KB) is a fixed tax. If bundle size is your primary constraint, no React framework can compete with Svelte's compiler approach.

Route Transition Bundle Sizes

After initial load, subsequent route transitions fetch only the new route's code. This is where code-splitting matters:

Framework Avg Route Chunk Size Prefetching Cache Strategy
TanStack Start 5-15KB Link hover TanStack Query SWR
Next.js 16 8-20KB Link viewport fetch cache + RSC payload
React Router v7 5-15KB Manual Manual
SvelteKit 3-8KB Link hover Built-in
Astro Full page* Prefetch API Browser cache

*Astro with View Transitions prefetches full pages but only swaps the changed content.


Core Web Vitals by Framework

Based on aggregated data from the Chrome User Experience Report and framework-specific benchmarks, here are typical Core Web Vitals scores for well-built sites:

Content/Marketing Site (Blog, Docs, Landing Page)

Metric TanStack Start Next.js 16 SvelteKit Astro
LCP 1.2-1.8s 0.8-1.4s 1.0-1.5s 0.5-1.0s
INP 50-120ms 60-150ms 30-80ms 20-50ms
CLS 0.02-0.05 0.01-0.03 0.02-0.05 0.01-0.02

Astro wins here because it ships zero or minimal JS. Next.js is strong due to next/image and next/font. TanStack Start has no built-in image optimization.

SaaS Dashboard (Data Tables, Charts, Forms)

Metric TanStack Start Next.js 16 SvelteKit Astro
LCP 1.0-1.5s 1.2-2.0s 0.8-1.2s N/A*
INP 60-130ms 80-200ms 40-100ms N/A*
CLS 0.01-0.03 0.02-0.05 0.01-0.03 N/A*

*Astro is not recommended for dashboard use cases.

TanStack Start edges ahead of Next.js for dashboards because its smaller router and TanStack Query integration reduce main-thread work. SvelteKit wins on raw numbers but requires the Svelte ecosystem.

E-Commerce (Product Pages, Cart, Checkout)

Metric TanStack Start Next.js 16 SvelteKit Astro
LCP 1.0-1.6s 0.8-1.2s 0.9-1.4s 0.6-1.0s
INP 70-150ms 80-180ms 40-100ms 30-80ms
CLS 0.02-0.04 0.01-0.02 0.02-0.04 0.01-0.02

Next.js wins e-commerce because ISR + image optimization + PPR are purpose-built for product pages. Astro with selective islands is the performance champion for catalog-style pages.


Data Fetching and Caching Patterns

How each framework handles data fetching has a direct impact on client-side performance. The framework's approach determines whether users see stale data, how often the browser makes network requests, and how much JavaScript is needed for the data layer.

TanStack Start: Built-In Query Integration

TanStack Start integrates natively with TanStack Query, giving you stale-while-revalidate (SWR) caching, background refetching, and deduplication out of the box:

// routes/dashboard.tsx
import { createFileRoute } from '@tanstack/react-router'
import { useSuspenseQuery, queryOptions } from '@tanstack/react-query'
import { getMetrics } from '../server/metrics'

const metricsOptions = (range: string) =>
  queryOptions({
    queryKey: ['metrics', range],
    queryFn: () => getMetrics({ data: { range } }),
    staleTime: 30_000,       // Consider fresh for 30 seconds
    gcTime: 5 * 60_000,      // Keep in cache for 5 minutes
    refetchInterval: 60_000, // Background refetch every minute
  })

export const Route = createFileRoute('/dashboard')({
  loader: ({ context: { queryClient } }) =>
    queryClient.ensureQueryData(metricsOptions('30d')),
  component: Dashboard,
})

function Dashboard() {
  const [range, setRange] = useState('30d')

  // Data is already cached from the loader — instant render
  // Subsequent range changes trigger SWR: show stale, fetch fresh
  const { data, isFetching } = useSuspenseQuery(metricsOptions(range))

  return (
    <div>
      {isFetching && <span className="refresh-indicator" />}
      <MetricsGrid data={data} />
    </div>
  )
}

The critical detail: the loader prefetches data during SSR, and TanStack Query's cache is hydrated on the client. Subsequent navigations to the same route reuse cached data instantly while refetching in the background.

Next.js: Fetch Cache + RSC

Next.js has its own caching layer built around the fetch API and React Server Components. In v16, the use cache directive replaced the implicit caching model:

// app/dashboard/page.tsx
import { Suspense } from 'react'

async function MetricsPanel({ range }: { range: string }) {
  'use cache'
  const res = await fetch(`https://api.example.com/metrics?range=${range}`, {
    next: { revalidate: 60 }, // Revalidate every 60 seconds
  })
  const data = await res.json()
  return <MetricsGrid data={data} />
}

export default function DashboardPage({
  searchParams,
}: {
  searchParams: Promise<{ range?: string }>
}) {
  const { range = '30d' } = await searchParams
  return (
    <Suspense fallback={<MetricsSkeleton />}>
      <MetricsPanel range={range} />
    </Suspense>
  )
}

The Next.js cache operates at the server level. Client-side caching for interactive components still requires a separate solution (React Query, SWR, or manual state management).

SvelteKit: Load Functions + Invalidation

SvelteKit uses load functions that run on the server and pass data to components. Client-side caching is minimal -- SvelteKit refetches data on every navigation by default:

// src/routes/dashboard/+page.server.ts
export const load: PageServerLoad = async ({ depends }) => {
  depends('app:metrics') // Register dependency for invalidation
  const metrics = await db.getMetrics('30d')
  return { metrics }
}
<script lang="ts">
  import { invalidate } from '$app/navigation'

  export let data

  // Manual refetch by invalidating the dependency
  function refresh() {
    invalidate('app:metrics')
  }
</script>

SvelteKit's approach is simpler but less powerful than TanStack Query. You do not get automatic background refetching, SWR, or request deduplication without adding a library.

Caching Strategy Comparison

Capability TanStack Start Next.js 16 React Router v7 SvelteKit Astro
SWR (stale-while-revalidate) Built-in (Query) Manual Manual Manual N/A
Background refetch Built-in Manual Manual Manual N/A
Request deduplication Built-in Server-side only Manual Manual N/A
Optimistic updates Built-in (mutations) Server Actions Form revalidation use:enhance N/A
Cache persistence In-memory (client) Server + CDN None None CDN only
Cache hydration (SSR to client) Automatic Automatic (RSC) Manual Automatic N/A

Key insight: If your application requires frequent data updates (dashboards, feeds, collaboration tools), TanStack Start's built-in Query integration saves significant client-side code. In Next.js, you typically add React Query anyway for interactive data -- at which point you are paying for both the RSC cache AND the Query cache.


State Management Across Frameworks

Client-side state management is where framework philosophy shows up most clearly. The question is not which state library to use, but how much state management the framework handles for you and how much remains your responsibility.

URL State

The most underrated form of state management. URL search parameters are shareable, bookmarkable, and survive page refreshes. Each framework handles them differently:

// TanStack Start — Validated, typed URL state
const Route = createFileRoute('/search')({
  validateSearch: z.object({
    q: z.string().default(''),
    page: z.number().default(1),
    sort: z.enum(['relevance', 'date', 'price']).default('relevance'),
  }),
})

// Next.js — Manual parsing, no validation
const searchParams = useSearchParams()
const q = searchParams.get('q') || ''
const page = parseInt(searchParams.get('page') || '1', 10)

// SvelteKit — Via $page store
import { page } from '$app/stores'
$: q = $page.url.searchParams.get('q') || ''

// React Router v7 — Via useSearchParams
const [searchParams] = useSearchParams()
const q = searchParams.get('q') || ''

TanStack Router's search parameter validation is unique. Every other framework treats search params as untyped strings.

Server State vs Client State

Framework Server State Pattern Client State Pattern Overlap
TanStack Start Server functions + Query cache React state + Query Minimal -- Query bridges both
Next.js RSC + Server Actions React state + external lib Significant -- RSC and client state are separate worlds
React Router v7 Loaders + Actions React state Moderate -- loaders handle most server state
SvelteKit Load functions + Actions Svelte stores Minimal -- load functions handle server state cleanly
Astro Build-time data fetching Per-island state None -- islands are isolated

When You Need a State Library

Not every application needs Zustand, Jotai, or Redux. Here is when you actually need one:

Situation Framework-Provided Solution Need External Library?
Server data on screen Loader/Query (all frameworks) No
Form input state useState/bind (all frameworks) No
URL-driven filters Search params (all frameworks) No
Cross-component UI state (modals, sidebars) Context/stores Usually no
Complex client-only state (drag-and-drop, canvas) None Yes -- Zustand, Jotai, Svelte stores
Real-time collaboration None Yes -- WebSocket + state sync
Undo/redo history None Yes -- Immer or custom

Most dashboard applications can get by with URL state for filters, TanStack Query (or equivalent) for server data, and React context or Svelte stores for UI state. Adding Redux or MobX to a new project in 2026 is almost always unnecessary complexity.


Middleware and Auth Patterns

Authentication affects client-side architecture because it determines what JavaScript ships for protected vs. public routes, how redirects work, and whether the auth check happens before or after hydration.

TanStack Start: Middleware + Server Functions

TanStack Start provides middleware that runs before route handlers:

// middleware/auth.ts
import { createMiddleware } from '@tanstack/react-start'
import { getSession } from '../server/auth'

export const authMiddleware = createMiddleware().server(async ({ next }) => {
  const session = await getSession()

  if (!session) {
    throw redirect({ to: '/login' })
  }

  return next({ context: { user: session.user } })
})

// routes/dashboard.tsx — Protected route
export const Route = createFileRoute('/dashboard')({
  beforeLoad: ({ context }) => {
    // Context from middleware is available and typed
    if (!context.user) throw redirect({ to: '/login' })
  },
  loader: ({ context: { user, queryClient } }) =>
    queryClient.ensureQueryData(dashboardOptions(user.orgId)),
  component: Dashboard,
})

The auth check runs on the server during SSR and during client-side navigation. Protected route code is still downloaded (code-split per route), but unauthenticated users are redirected before the component renders.

Next.js: Middleware + Server Components

Next.js uses edge middleware that runs before any route:

// middleware.ts (runs at the edge, before SSR)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const token = request.cookies.get('session')

  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
}

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*'],
}

Next.js middleware runs at the CDN edge and can redirect before the server even starts rendering. This is faster for unauthenticated users but adds complexity for authenticated state that needs to flow into components.

SvelteKit: Hooks

SvelteKit uses hooks.server.ts to intercept all requests:

// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit'
import { redirect } from '@sveltejs/kit'

export const handle: Handle = async ({ event, resolve }) => {
  const session = await getSession(event.cookies)

  if (session) {
    event.locals.user = session.user
  }

  // Protect routes
  if (event.url.pathname.startsWith('/dashboard') && !event.locals.user) {
    throw redirect(303, '/login')
  }

  return resolve(event)
}

Client-Side Impact of Auth Patterns

Framework Auth Check Location Client Impact
TanStack Start Server function / beforeLoad Redirect happens before component code loads; auth state in router context
Next.js Edge middleware / RSC Redirect at CDN edge (fastest); auth state must be passed through RSC
React Router v7 Loader Redirect happens server-side; auth state via loader data
SvelteKit hooks.server.ts Redirect server-side; auth state via locals
Astro Middleware / page frontmatter Redirect server-side; no client auth state needed for static pages

Key insight: Next.js edge middleware provides the fastest auth redirect (CDN level), but TanStack Start's typed middleware context gives the cleanest DX for passing auth state through the application. For most applications, the performance difference is negligible -- the DX difference is not.


Migration Paths

If you are moving from one framework to another, here are realistic migration strategies and effort estimates.

From Next.js Pages Router to TanStack Start

This is the smoothest migration path because Pages Router components are already client-side React. Steps:

  1. Replace getServerSideProps / getStaticProps with TanStack Start loaders
  2. Convert pages/ directory to TanStack Router file-based routes
  3. Replace next/router with TanStack Router hooks
  4. Convert API routes to createServerFn server functions
  5. Replace next/image and next/font with manual alternatives or Vite plugins

Estimated effort: 2-4 weeks for a medium application (20-40 routes).

From Next.js App Router to TanStack Start

Harder because you must convert Server Components back to client components:

  1. Remove "use client" directives (everything is client in TanStack Start)
  2. Convert Server Components to loaders + client components
  3. Replace Server Actions with createServerFn
  4. Replace RSC-based data patterns with TanStack Query
  5. Handle image/font optimization manually

Estimated effort: 4-8 weeks for a medium application. RSC patterns do not have 1:1 equivalents.

From Remix / React Router v7 to TanStack Start

TanStack provides an official migration guide. Key steps:

  1. Replace loader / action exports with TanStack loaders and server functions
  2. Convert route definitions to TanStack Router format
  3. Replace <Form> components with standard forms + server function calls
  4. Add Zod schemas for search parameter validation

Estimated effort: 1-3 weeks. Conceptually similar but different APIs.

From Any React Framework to SvelteKit

This is a rewrite, not a migration. You are changing languages:

  1. Convert all React components to Svelte components
  2. Replace React hooks with Svelte stores / runes
  3. Rewrite data fetching as SvelteKit load functions
  4. Port CSS (if using CSS-in-JS, switch to Svelte's scoped styles)
  5. Find Svelte alternatives for React-specific libraries

Estimated effort: 6-12 weeks for a medium application. Plan for it to take twice as long as you think.

Migration Effort Matrix

From / To TanStack Start Next.js 16 SvelteKit Astro
Next.js Pages 2-4 weeks 4-6 weeks (to App Router) 6-12 weeks 2-4 weeks (content only)
Next.js App 4-8 weeks N/A 8-16 weeks 3-6 weeks (content only)
Remix / RR v7 1-3 weeks 3-6 weeks 6-12 weeks 2-4 weeks (content only)
SvelteKit 4-8 weeks 4-8 weeks N/A 2-4 weeks (content only)
CRA / Vite SPA 1-2 weeks 2-4 weeks 4-8 weeks N/A (not suitable)

Use Case Decision Matrix

"I am building a SaaS dashboard"

Recommended: TanStack Start (React) or SvelteKit (non-React)

Rationale: Dashboards are client-heavy applications. You need fast route transitions, client-side caching, and minimal framework overhead. TanStack Start's built-in Query integration and type-safe routing are designed for this. SvelteKit wins if you are willing to leave React.

Next.js is viable but the RSC model adds complexity for interactive applications. The "server-first" default fights against dashboard UX patterns.

"I am building a marketing site"

Recommended: Astro (static-first) or Next.js (dynamic content)

Rationale: Marketing sites are content-heavy with isolated interactivity. Astro's zero-JS default and island architecture are ideal. If you need dynamic features (personalization, A/B testing, auth), Next.js with ISR and image optimization is the mature choice.

"I am building a blog or documentation site"

Recommended: Astro (specifically Starlight for docs)

Rationale: Zero JavaScript by default means perfect Lighthouse scores. Astro's content collections make managing Markdown/MDX straightforward. Nothing else comes close for this use case.

"I need the smallest possible bundle"

Recommended: SvelteKit

Rationale: Svelte's compiler approach eliminates the framework runtime. No React framework can match ~2KB of framework overhead. If bundle size is a hard constraint (embedded widgets, low-bandwidth markets), Svelte is the only serious option.

"I am hiring senior React developers"

Recommended: Next.js or TanStack Start

Rationale: Next.js has the largest talent pool. TanStack Start is React-based and increasingly familiar to developers who use TanStack Query (downloaded ~10M times weekly). Both are defensible choices for team scalability.

"I deploy to Cloudflare Workers"

Recommended: TanStack Start or Astro

Rationale: TanStack Start has native Cloudflare Workers support via Nitro. Astro is now owned by Cloudflare. Next.js on Workers requires OpenNext and sacrifices features.

"I need progressive enhancement"

Recommended: React Router v7 (framework mode) or SvelteKit

Rationale: These are the only frameworks where forms work without JavaScript. If you are building for government, accessibility-critical, or unreliable-network contexts, progressive enhancement is not optional.

"I am migrating from an existing React SPA"

Recommended: TanStack Start

Rationale: If you have an existing Create React App or Vite SPA, TanStack Start is the easiest migration because it is client-first. You can add SSR and server functions incrementally without rewriting your components. Next.js App Router would require converting most components to either Server Components or "use client" annotated modules.


Anti-Patterns

These mistakes waste bundle budget regardless of which framework you choose:

Do Not Do Instead Why
Import entire icon libraries (import * as Icons from 'lucide-react') Import individual icons (import { Search } from 'lucide-react') Tree-shaking cannot eliminate unused icons from barrel exports in all bundlers
Use moment.js for date formatting Use date-fns or Intl.DateTimeFormat moment.js is 70KB gzipped and not tree-shakeable
Load all chart data on initial render Lazy-load chart libraries with React.lazy() or dynamic imports Chart libraries (Recharts, Chart.js) are 40-80KB; defer until the user sees them
Put everything in "use client" (Next.js) Keep client components at leaf nodes; pass server data via props Every "use client" component adds to the client bundle
Skip route-based code splitting Use file-based routing (all frameworks support this) Without splitting, users download code for routes they never visit
Bundle environment-specific polyfills Use browserslist and let the bundler handle it Shipping polyfills for features 95%+ of browsers support wastes bytes
Inline large JSON datasets in HTML Fetch data after hydration or use streaming SSR Large inline JSON blocks increase HTML size and parse time
Use CSS-in-JS runtime libraries (styled-components, emotion) Use static CSS extraction (Tailwind, CSS Modules, vanilla-extract) Runtime CSS-in-JS adds 10-15KB and increases hydration time
Skip image optimization Use framework-provided image components or manual srcset Unoptimized images are the largest LCP bottleneck on most sites
Fetch data in useEffect on every component mount Use a caching layer (TanStack Query, SWR, or framework loaders) Redundant fetches increase TTI and create layout shift
Use a full state management library for simple UI state Use URL state (search params) for filter/sort state URL state is free, shareable, and survives refresh
Ship development-only code to production Ensure NODE_ENV=production and remove debug panels React DevTools hooks, console logs, and debug boundaries add 5-10KB

Decision Framework

Ask these questions in order. Each answer narrows your options:

1. How interactive is your application?

Mostly static content → Astro
Mix of content + interactivity → Next.js or SvelteKit
Highly interactive (dashboard, editor, real-time) → TanStack Start or SvelteKit

2. Must you use React?

Yes (team skills, library needs, hiring) → TanStack Start or Next.js
No preference → SvelteKit (best performance) or Astro (best for content)

3. Where do you deploy?

Cloudflare → TanStack Start or Astro
Vercel → Next.js (best optimization) or any
Multiple platforms → TanStack Start or SvelteKit
Static CDN → Astro

4. How important is type safety?

Critical (large team, complex data) → TanStack Start (strongest)
Important → Any modern framework
Not a priority → Astro

5. Do you need progressive enhancement?

Yes → React Router v7 or SvelteKit
No → Any framework

6. What is your bundle budget?

<50KB → SvelteKit or Astro
<100KB → TanStack Start or SvelteKit
<150KB → Any framework
No constraint → Next.js (most features)

7. Are you migrating or starting fresh?

Migrating from CRA / Vite SPA → TanStack Start (easiest path)
Migrating from Next.js Pages Router → TanStack Start or Next.js App Router
Migrating from Remix → TanStack Start or React Router v7
Starting fresh → Evaluate all options based on Q1-Q6

Hidden Costs to Consider

Framework Hidden Cost
TanStack Start Fewer tutorials, smaller community, edge cases in young framework
Next.js Vercel lock-in for optimal performance, App Router complexity, RSC learning curve
React Router v7 Uncertain future (Remix team focused on v3), less opinionated than alternatives
SvelteKit Smaller hiring pool, fewer component libraries, team retraining cost
Astro Not suitable for applications, limited client-side routing, island state sharing is complex

Conclusion

There is no universal "best" framework in 2026. The landscape has stratified into clear use-case lanes:

For SaaS dashboards and interactive React applications, TanStack Start offers the best combination of type safety, bundle efficiency, and deployment flexibility. Its client-first model aligns with how dashboards actually work.

For content-heavy sites and marketing pages, Astro's zero-JS default is unbeatable. With Cloudflare backing, it is a safe long-term choice.

For maximum client-side performance regardless of ecosystem, SvelteKit ships 3-5x less JavaScript than any React framework. The ecosystem trade-off is real, but the performance advantage is structural.

For teams deeply invested in Vercel and React, Next.js remains the safest choice. Its ecosystem, tooling, and image optimization are mature. The App Router complexity is the price of entry.

For progressive enhancement, React Router v7 with framework mode carries the Remix torch. It is the only React framework where forms work without JavaScript.

The most important decision is not which framework to pick -- it is which trade-offs you can live with. Every framework sacrifices something. Know what you are giving up, and make sure it is not the thing your users need most.


References

Official Documentation

Deployment and Infrastructure

Comparison Articles and Analysis

Remix and React Router Evolution

Performance and Benchmarks

Framework Architecture

Top categories

Loading Svelte Themes