React vs Angular vs Vue (2026): bundle size, build time and which to pick - NextGenBeing
Back to discoveries

React vs Angular vs Vue (2026): bundle size, build time and which to pick

Same todo app built in React 19, Vue 3.5 and Angular 22: measured bundle size, build time and code size, runnable scripts, and which framework to pick.

Operating Systems Premium Content 10 min read
Bekzod Erkinov

Bekzod Erkinov

Oct 11, 2026 • 97 views
React vs Angular vs Vue (2026): bundle size, build time and which to pick
Photo by John Doe on Unsplash
Size:
Height:
10 min read 4,287 words Focus mode: Eye care:

Listen to Article

Loading...
0:00 / 0:00
0:00 0:00
Low High
0% 100%
⏸ Paused ▶️ Now playing... Ready to play ✓ Finished

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

Join 10,000+ developers who love our premium content

Bekzod Erkinov

Bekzod Erkinov

Author

Founder of NextGenBeing. Software engineer working with Laravel, Python, and cloud infrastructure. Writes about patterns that actually hold up in production. Based in Tashkent, Uzbekistan.

Free guide

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

Related Articles

Don't miss the next deep dive

Get one well-researched tutorial in your inbox each week. No spam, unsubscribe anytime.