Choose by project, not popularity. Astro suits content sites because pages ship as HTML with little or no JavaScript. Next.js, built on React, suits full web apps. Vue is the gentlest to learn, and Svelte ships the least code. We built one todo app in each: Svelte shipped 13.8 KB gzipped, Vue 25.8 KB and React 68 KB.
| Tool | Best at |
|---|---|
| React | Apps, with the largest ecosystem |
| Next.js | Full React apps with server rendering |
| Vue | Approachable apps with templates |
| Svelte | Small bundles, little boilerplate |
| Astro | Content sites that ship little JavaScript |
Updated
Opinions are easy to find; measurements of the same app are not. We wrote one small todo app in React, Vue, Svelte and plain JavaScript, built each with Vite, clicked through it in a browser and measured what it ships. Astro and Next.js are shown with the same component in place.
Building a content site? Also see static site generators.
Built with Vite in production mode; every build was tested in a browser. Astro and Next.js were not part of this measurement.
| Setup | Minified | Gzipped |
|---|---|---|
| Plain JavaScript | 1.4 KB | 0.7 KB |
| Svelte 5 | 35.2 KB | 13.8 KB |
| Vue 3 | 66.4 KB | 25.8 KB |
| React 19 (with react-dom) | 220.1 KB | 68.0 KB |
| Project | Good choice | Why |
|---|---|---|
| Marketing site, blog, docs | Astro | Pages ship as HTML with no JavaScript unless a part needs it |
| Web app with logins and dashboards | Next.js or React Router (React) | Large ecosystem, server rendering and data loading built in |
| App where the team prefers templates | Vue with Nuxt | Gentle learning curve, single-file components |
| Small, fast widgets or a lean app | Svelte with SvelteKit | Compiler output is small; little boilerplate |
| A few interactive bits on an existing site | Plain JavaScript or a small island | No framework cost at all |
| Hiring many developers quickly | React | The largest pool of people and libraries |
| React | Vue | Svelte | Astro | Next.js | |
|---|---|---|---|---|---|
| What it is | UI library | UI framework | Compiler + framework | Content-site framework | React framework |
| Components written as | JSX functions | Single-file .vue | Single-file .svelte | .astro + any UI library | React (JSX) |
| Reactivity | State + re-render | Proxies (ref, reactive) | Compiled runes ($state) | Per island | React |
| Server rendering | Through a framework | Through Nuxt | Through SvelteKit | Built in (static or server) | Built in |
| Licence | MIT | MIT | MIT | MIT | MIT |
State lives in hooks; every change re-runs the component and React updates the DOM differences. Updates must create new arrays and objects.
import { useState } from "react";
export default function App() {
const [todos, setTodos] = useState([]);
const [text, setText] = useState("");
const left = todos.filter((t) => !t.done).length;
function add(e) {
e.preventDefault();
if (!text.trim()) return;
setTodos([...todos, { id: Date.now(), text, done: false }]);
setText("");
}
return (
<form onSubmit={add}>
<label htmlFor="new">New task</label>
<input id="new" value={text} onChange={(e) => setText(e.target.value)} />
<button>Add</button>
<ul>
{todos.map((t) => (
<li key={t.id}>
<label>
<input type="checkbox" checked={t.done}
onChange={() => setTodos(todos.map((x) => x.id === t.id ? { ...x, done: !x.done } : x))} />
{t.text}
</label>
</li>
))}
</ul>
<p>{left} left</p>
</form>
);
}
Script, template and (optionally) styles in one file. v-model binds inputs both ways and refs are reactive, so pushing to the array is enough.
<script setup>
import { computed, ref } from "vue";
const todos = ref([]);
const text = ref("");
const left = computed(() => todos.value.filter((t) => !t.done).length);
function add() {
if (!text.value.trim()) return;
todos.value.push({ id: Date.now(), text: text.value, done: false });
text.value = "";
}
</script>
<template>
<form @submit.prevent="add">
<label for="new">New task</label>
<input id="new" v-model="text" />
<button>Add</button>
<ul>
<li v-for="t in todos" :key="t.id">
<label><input type="checkbox" v-model="t.done" /> {{ t.text }}</label>
</li>
</ul>
<p>{{ left }} left</p>
</form>
</template>
Runes ($state, $derived) mark reactive values, and the compiler turns the component into direct DOM updates, which is why its runtime is small.
<script>
let todos = $state([]);
let text = $state("");
let left = $derived(todos.filter((t) => !t.done).length);
function add(e) {
e.preventDefault();
if (!text.trim()) return;
todos.push({ id: Date.now(), text, done: false });
text = "";
}
</script>
<form onsubmit={add}>
<label for="new">New task</label>
<input id="new" bind:value={text} />
<button>Add</button>
<ul>
{#each todos as t (t.id)}
<li><label><input type="checkbox" bind:checked={t.done} /> {t.text}</label></li>
{/each}
</ul>
<p>{left} left</p>
</form>
The baseline. Fine for a small widget, but you write the DOM updates yourself and keep them in sync with the data by hand.
const app = document.getElementById("app");
app.innerHTML = `<form><label for="new">New task</label><input id="new"><button>Add</button><ul></ul><p></p></form>`;
const form = app.querySelector("form"), input = app.querySelector("#new"), list = app.querySelector("ul"), count = app.querySelector("p");
const todos = [];
function render() {
list.replaceChildren(...todos.map((t) => {
const li = document.createElement("li");
const label = document.createElement("label");
const box = Object.assign(document.createElement("input"), { type: "checkbox", checked: t.done });
box.addEventListener("change", () => { t.done = box.checked; render(); });
label.append(box, " ", t.text);
li.append(label);
return li;
}));
count.textContent = `${todos.filter((t) => !t.done).length} left`;
}
form.addEventListener("submit", (e) => {
e.preventDefault();
if (!input.value.trim()) return;
todos.push({ id: Date.now(), text: input.value, done: false });
input.value = "";
render();
});
render();
Astro renders the page to HTML and only hydrates components given a client directive. Here the Svelte todo app loads its JavaScript when it scrolls into view; the rest of the page ships none.
---
// src/pages/index.astro
import Layout from "../layouts/Layout.astro";
import Todo from "../components/Todo.svelte";
---
<Layout title="My tasks">
<h1>My tasks</h1>
<p>Plain HTML, no JavaScript.</p>
<Todo client:visible />
</Layout>In the App Router the page is a Server Component, so the interactive todo list goes in its own file marked "use client". The page around it sends no component JavaScript of its own.
// app/page.tsx (Server Component)
import Todo from "./todo"; // todo.tsx starts with "use client" and holds the React code above
export default function Page() {
return (
<main>
<h1>My tasks</h1>
<Todo />
</main>
);
}| Vite | webpack | |
|---|---|---|
| Dev server | Serves source as native ES modules; starts fast regardless of app size | Bundles before serving; start-up grows with the app |
| Production build | Bundles with its own bundler (Rolldown in Vite 8) | Its own bundler, highly configurable |
| Configuration | Small config, sensible defaults | Powerful, more to write |
| Where you meet it | Vue, Svelte, Astro, React Router and many React apps | Older React apps, Next.js (alongside Turbopack) |
Vue and Svelte read closest to plain HTML, CSS and JavaScript, so beginners get going quickly. React needs JSX, hooks and the habit of immutable updates. Astro is easy for content but asks you to understand islands once you add interaction. Next.js adds server and client boundaries on top of React, which is the steepest step of the five.
Static output from Astro, Vite apps, or Next.js static export can go on any static host or CDN. Server rendering needs a Node-capable host or a platform with serverless functions; check what your host supports before you choose server features.
No. React is the UI library; Next.js is a framework built on React that adds routing, server rendering, Server Components, data loading and a build setup. You write React components inside Next.js.
For content-led sites such as marketing pages, blogs and docs, Astro usually ships less JavaScript and is simpler. For app-like sites with logins, dashboards and lots of interaction, Next.js fits better.
Many people find it easier to start with, because templates look like HTML and reactivity needs no immutable updates. React asks you to learn JSX and hooks rules first but has more libraries and jobs.
Svelte compiles components into plain DOM update code at build time, so it ships a small runtime instead of a general rendering engine.
For a new project, Vite: it starts quickly and needs little configuration. webpack still makes sense in existing projects that rely on its plugins.
Related: Static site generators, React Server Components, How to build a web app, Website vs web app, All free tools.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.