Bekzod Erkinov
Listen to Article
Loading...React vs Angular vs Vue (2026): bundle size, build time and which to pick
Most "react vs angular vs vue" articles are feature tables with no measurements. I built the same small todo app in React 19.3, Vue 3.5 and Angular 22, then measured production bundle size, build time, lines of code, install footprint and the framework concepts each version needed. The source of all three apps and every script is in this post, so you can rerun it.
Results in three lines (one Windows 11 machine, Node 24.19.0, 2026-10-11):
- Brotli bundle: Vue 23.7 kB, Angular 35.2 kB, React 59.1 kB.
- Cold production build (median of 5, calling the tool directly): React 1.54 s, Vue 1.75 s, Angular 8.70 s.
node_modules: React 43.7 MB, Vue 52.1 MB, Angular 245.5 MB.
Not like-for-like. The React number is react-dom with no router or framework. Angular's figure is its compiled runtime and Vue's is its reactivity core. Next.js, Nuxt or Angular SSR would shift every number here; I did not measure them.
Limits. This is a toy app of 31 to 51 non-blank lines per framework, each written once, measured on one machine with noisy timings. Read the numbers as orders of magnitude.
What I built
A todo list with add, toggle, delete, an all/active/done filter, a "N left" counter and localStorage persistence. The CSS file is byte-identical across all three apps. None of the three uses a router or a state library.
| Version | Scaffold | |
|---|---|---|
| React | react 19.3.0 |
Vite 8.3.4 + @vitejs/plugin-react 6.1.2, .jsx |
| Vue | vue 3.5.43 |
Vite 8.3.4 + @vitejs/plugin-vue 6.0.10, `` SFC |
| Angular | @angular/core 22.2.2 |
ng new --defaults, standalone component, signals, TypeScript |
Scaffold and install commands (the versions above are what ended up installed):
npm create vite@latest react-app -- --template react
npm create vite@latest vue-app -- --template vue
npx @angular/cli new ng-app --defaults
cd react-app && npm install && cd ..
cd vue-app && npm install && cd ..
ng new installs its own dependencies. Then replace the generated sources with the listings below. React's own docs recommend starting with a framework and treat plain Vite as the build-it-yourself route. I used Vite as the closest like-for-like for a client-side app, so the React number is React with no framework around it. Angular is TypeScript by default and I left it that way, while React and Vue are plain JavaScript.
The same component in all three
The part that differs most is state. Here is the toggle-and-persist logic in each.
React (App.jsx):
const [todos, setTodos] = useState(() => JSON.parse(localStorage.getItem('todos') ?? '[]'));
useEffect(() => localStorage.setItem('todos', JSON.stringify(todos)), [todos]);
// toggle, inside the list item:
onChange={() => setTodos(todos.map((x) => (x.id === t.id ? { ...x, done: !x.done } : x)))}
Vue (App.vue, ``):
const todos = ref(JSON.parse(localStorage.getItem('todos') ?? '[]'));
watch(todos, (v) => localStorage.setItem('todos', JSON.stringify(v)), { deep: true });
// toggle, in the template: no handler at all
//
Angular (app.ts):
@Component({ selector: 'app-root', templateUrl: './app.html' })
export class App {
todos = signal(JSON.parse(localStorage.getItem('todos') ?? '[]'));
constructor() {
effect(() => localStorage.setItem('todos', JSON.stringify(this.todos())));
}
toggle(id: number) {
this.todos.update((l) => l.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
}
}
React and Angular replace the array on every toggle (map plus a spread). Vue's v-model mutates t.done on a reactive proxy. That is most of the line-count difference below.
Full listings of all three apps
react-app/src/App.jsx (main.jsx is createRoot(document.getElementById('root')).render() plus the CSS import, vite.config.js is defineConfig({ plugins: [react()] })):
import { useEffect, useState } from 'react';
const FILTERS = ['all', 'active', 'done'];
export default function App() {
const [todos, setTodos] = useState(() => JSON.parse(localStorage.getItem('todos') ?? '[]'));
const [filter, setFilter] = useState('all');
const [text, setText] = useState('');
useEffect(() => localStorage.setItem('todos', JSON.stringify(todos)), [todos]);
const visible = todos.filter((t) => filter === 'all' || (filter === 'done') === t.done);
const remaining = todos.filter((t) => !t.done).length;
function add(e) {
e.preventDefault();
const value = text.trim();
if (!value) return;
setTodos([...todos, { id: Date.now(), text: value, done: false }]);
setText('');
}
return (
<h1>Todos</h1>
setText(e.target.value)} />
Add
<ul>
{visible.map((t) => (
<li key={t.id} className={t.done ? 'done' : ''}>
setTodos(todos.map((x) => (x.id === t.id ? { ...x, done: !x.done } : x)))} />
{t.text}
setTodos(todos.filter((x) => x.id !== t.id))}>x
</li>
))}
</ul>
{remaining} left
{FILTERS.map((f) => (
setFilter(f)}>{f}
))}
);
}
vue-app/src/App.vue (main.js is createApp(App).mount('#app') plus the CSS import, vite.config.js uses vue() from @vitejs/plugin-vue):
import { computed, ref, watch } from 'vue';
const FILTERS = ['all', 'active', 'done'];
const todos = ref(JSON.parse(localStorage.getItem('todos') ?? '[]'));
const filter = ref('all');
const text = ref('');
watch(todos, (v) => localStorage.setItem('todos', JSON.stringify(v)), { deep: true });
const visible = computed(() =>
todos.value.filter((t) => filter.value === 'all' || (filter.value === 'done') === t.done),
);
const remaining = computed(() => todos.value.filter((t) => !t.done).
Unlock Premium Content
You've read 30% of this article
What's in the full article
- Complete step-by-step implementation guide
- Working code examples you can copy-paste
- Advanced techniques and pro tips
- Common mistakes to avoid
- Real-world examples and metrics
Don't have an account? Start your free trial
Join 10,000+ developers who love our premium content
Keep reading
Advanced Laravel Tutorial: Queues, Events and Pest Tests
22 min · 198 views
Artificial IntelligenceReact vs Angular: Which Framework to Choose in 2026
26 min · 183 views
AI WorkflowsMulti-Tenant SaaS Architecture with Laravel and React: Battle-Tested Patterns from Production
33 min · 170 views
Bekzod Erkinov
AuthorFounder of NextGenBeing. Software engineer working with Laravel, Python, and cloud infrastructure. Writes about patterns that actually hold up in production. Based in Tashkent, Uzbekistan.
Get the AI-Assisted Developer's Field Guide
The workflow, prompts, and tools I use to ship faster with AI — free when you subscribe. Plus new deep-dives in your inbox. No spam, unsubscribe anytime.
Comments (0)
Please log in to leave a comment.
Log In