Every "Blazor vs React vs Angular" page on the first result pages is an opinion with a feature table. This series is a measurement. The same todo app was built four times, in Blazor WebAssembly, Blazor Server, React 19 and Angular 22, by headless Claude Code sessions working from one specification, against one API, judged by one frozen test suite. Then every client was measured the same way: bytes, startup, a 10,000-row list, memory, lines of code, accessibility, and how many turns the AI needed. This part is the method. Read it before the numbers, because the numbers only mean what the method lets them mean.

Why a fourth client, and why a fifth

Blazor is two frameworks. WebAssembly downloads a .NET runtime and runs in the browser; Server renders on the server and ships DOM patches over a WebSocket. They fail differently, so both are full clients here, built independently from the same spec, not one project with a switched render mode. The render-mode post explains the difference; this series measures it.

The fifth client is not a framework at all. A vanilla TypeScript app, written by hand together with the test suite, exists so that every test was proven against something before a single metered session ran, and so that the size and speed tables have a "no framework" row. Where a framework costs bytes or milliseconds, that row shows how many.

The app: Tasks, in fifteen steps

A todo manager with lists, tasks, a detail panel, filters, search and sort, drag-and-drop reorder, subtasks, bulk actions, undo, keyboard shortcuts, dark mode, import and export, recurring tasks, and one list of 10,000 tasks that has to stay usable. The spec cuts it into fifteen steps, each with its own tagged tests, and each client was built one step per session, in order.

The fifteen steps and the number of tests that guard each one (61 in all)
StepWhat it addsTests
0 Shellstylesheet wired, list sidebar with counts, rows with checkbox state, routes5
1 Listsadd, rename inline, delete with a confirm, selection in the URL5
2 Tasksadd, complete, edit the title inline, delete; every change through the API5
3 Detail panelnotes, due date, priority, tag chips, repeat; Escape closes5
4 FiltersAll, Today, Upcoming, Overdue, Completed with counts5
5 Search and sortsearch on every keystroke, five sort orders, all in the URL3
6 Drag-and-drop reorderdrag handles in manual sort, order saved3
7 Subtasksadd, toggle, delete, "2/5" progress on the row3
8 Bulk actionsselect, select all, complete, delete, move4
9 Undoa five-second toast that restores what was deleted or completed5
10 Keyboard shortcutsn, /, j, k, x, Delete, Enter, Escape, ? with a help dialog6
11 Dark modea toggle, localStorage, prefers-color-scheme on first load2
12 Import and exportreal JSON and CSV downloads, JSON upload3
13 Recurring tasksdaily, weekly, monthly; completing shows the next occurrence3
14 The 10,000-item listunder 200 rows in the DOM while the count says 10,000; everything still works4

What every client shares

The backend is not part of the comparison, so it was built once: an ASP.NET Core 10 minimal API with EF Core and SQLite, 21 endpoints, soft deletes, repeating tasks, bulk actions, export and import, and a deterministic seed. A fixed random seed decides every title, tag and priority; the due-date offsets are applied to the real date when the database is created, so "today" is always today and no client needs a mocked clock. Each client got its own instance on its own port, and POST /api/admin/reset restored the seed before every test. JSON responses are compressed, because the 10,000-task list is 3.5 MB raw and a slow link would have charged every client the same 2.8 seconds before any rendering began.

The look is shared too. One stylesheet of about 300 lines, one SVG sprite, no component kit on any side: no MudBlazor, no Radzen, no MUI, no Angular Material. The spec fixes the routes, 56 data-testid names, the class names, and two details that make the 10,000-row step comparable: the scroll container is the same element in every client and rows are 40 pixels tall, so every virtualization has the same job.

How the clients were built

Each client folder holds the official scaffold (dotnet new blazorwasm, dotnet new blazor --interactivity Server, npm create vite with the React template, ng new), a copy of the spec, a copy of the suite, the shared files, and a short CLAUDE.md. Then sixty headless sessions ran, one per step per client, each with the same prompt:

Read CLAUDE.md and SPEC.md. Implement step {NN} ({NAME}) of SPEC.md in this project.
Run the end-to-end tests for this step with the command in CLAUDE.md, fix the application
until they pass, and make sure the tests of every earlier step still pass. Do not edit
anything under e2e/ or shared/ and do not commit. When the tests pass, stop and print your
summary. If you cannot make them pass, stop and say exactly which tests fail and why.

The CLAUDE.md files are identical except for the stack block. This is React's; the other three are linked at the end:

Stack: React 19 with TypeScript (strict) on Vite 8, project folder `ReactClient/` (created
with `npm create vite@latest ReactClient -- --template react-ts`), package manager npm, the
template's linter kept. No UI component library.
Dev server: `npm run dev -- --port 16031 --strictPort` in `ReactClient/`. Do NOT start it
yourself: the test command below starts it for every run and stops it afterwards. To check
type and lint errors run `npx tsc -b` and `npm run lint` in `ReactClient/` without serving.
API: http://localhost:16030, already running. Never start, stop or change it.
Tests: `cd e2e; npx playwright test --grep @stepNN`   (NN = the step you are on, two digits)

Rules
- Implement only what SPEC.md asks for the current step. Earlier steps must stay green.
- Use the data-testid names, routes and class names from SPEC.md exactly.
- shared/app.css and shared/icons.svg are copied into the app unchanged (public/); add your
  own styles in a separate file only when a step needs them.
- Never edit anything under e2e/ or shared/. If a test looks wrong, say so in your summary
  and stop; do not work around it.
- Add a dependency only when the step needs it, and list every one in your summary.
- Do not commit. Do not touch files outside this folder.
- Finish with a short summary: files changed, dependencies added, anything you could not do.

The sessions ran Claude Code 2.1.287 with Opus 5.5 pinned, no user-level instruction files, a fixed list of allowed shell commands, the test and shared folders denied to the Edit and Write tools, a budget cap of six dollars and a wall clock of thirty minutes. Nothing the session said about its own tests counted. After each session the harness reverted any change under the test or shared folders, ran the frozen suite for that step and every earlier one, and committed the result, pass or fail. A failed verdict earned one retry with a second fixed prompt; a second failure would have meant a hand fix, recorded as such. The whole run, 64 sessions, needed none: every client passed all 61 tests. Four sessions failed their first verdict. Three of those retries changed nothing, ran the same tests and passed, which makes them flaky test runs, not framework failures, and the results post counts them separately. The fourth, Angular's step 9, fixed a real two-line bug on the way.

Two clients built at a time, 20:45 to 23:21 on one evening, about $38 on the meter. The meter is Claude Code's own total_cost_usd at list price; on a Max plan it is a measure, not a bill, and it is reported as one.

The suite was proven before it judged anyone

The 61 tests use only the test ids and routes from the spec and visible text, read every expected value from the API, and never touch a framework. Before the first metered session, the hand-written reference client passed all of them, and a mutation check broke fourteen features one at a time: the suite caught thirteen. The one it missed, tags not lower-cased in the client, is hidden by the API lower-casing them anyway. Where the spec leaves a choice, the suite accepts each reading, and the readings are listed in its README: Today with or without completed tasks, created-sort in either direction, a drop before or after the target row, delete-list through a native confirm or an in-page dialog. Its SHA-256 was recorded before the builds, and it did not change during them.

What is measured, and how

After all four clients passed, one measurement pass ran against production builds on the same static host (Brotli and gzip by negotiation; Blazor Server runs its own published process), in a fresh Chrome profile at 1366 by 768, nothing else running, five runs per metric, medians with min and max, on two network settings: unthrottled, and 100 ms round trip with 10 Mbit/s through Chrome's own emulation.

  • Payload: files and bytes on disk, requests and bytes on the wire for a cold load and a warm reload, Blazor WebAssembly with AOT as a side row.
  • Startup: Lighthouse 13, desktop and mobile, plus the time from navigation to the first visible task row.
  • Interactions on the 10,000-row list: input to paint for selecting the list, typing a search one key at a time, clearing it, sorting, toggling a checkbox, opening and closing the panel, select-all, and frames per second while scrolling.
  • Memory: JavaScript heap and DOM nodes after a forced garbage collection, and the WebAssembly linear memory for Blazor.
  • Developer metrics: cold build time, dev-server start, lines of code, files, dependencies, install size, suite run time.
  • Accessibility: axe on four states of each client.
  • AI metrics: turns, tool calls, tokens, meter, wall time, first-run passes, retries, hand fixes, dependencies added, per step and per client, from the session transcripts.
  • A use pass: twenty minutes in a real browser with each client, logging what the suite missed.

What is not compared

Server-side rendering frameworks (Next, Nuxt, Analog), SEO, component kits, mobile shells, team experience, hiring. One machine, one model, one spec, one week. A result here is a fact about these four builds under these rules, and a strong hint about the frameworks; it is not a law. Where a number depends on the AI's choice (a library, a virtualization approach), the posts say which choice it made.

Seven hypotheses, written before the first build

Each one is settled in the results post, including the ones that turned out wrong.

  1. First-load transfer of Blazor WebAssembly is at least five times React's.
  2. Unthrottled, median input-to-paint on the 10,000-row list is within a factor of two across Blazor WebAssembly, React and Angular.
  3. Blazor Server's per-interaction latency is the round trip plus a small constant, so at 100 ms it is the slowest client on every interaction while its first paint is the fastest.
  4. The AI needs the fewest retries and hand fixes for React and the most for Blazor Server.
  5. Angular produces the most files and lines of code; Blazor WebAssembly the fewest lines.
  6. Lighthouse mobile performance: React and Angular 90 or more, Blazor WebAssembly below 70, Blazor Server above 90.
  7. Across all four, the AI's most common mistake is wrong or missing test-id names, not framework errors.

Disclosure: this site runs on Blazor, static server-side rendering. The author writes .NET for a living and likes it. That is exactly why the comparison is a harness and a frozen suite rather than an opinion: the numbers do not care.

Do it yourself

Everything is public: the API, the suite, the reference client and the four AI-built clients with their per-step commits.

  1. Clone the repository and start the API, which seeds its own database on first start:
    git clone https://github.com/rahulvyas777/coder000-todo-four-ways.git
    cd coder000-todo-four-ways
    dotnet run --project api -- --urls http://localhost:16050 --db run/reference.db
  2. In a second terminal, run the reference client and open http://localhost:16051:
    cd reference
    npm install
    npm run dev -- --port 16051 --strictPort
  3. Run the suite against it (it uses your installed Chrome):
    cd harness/e2e
    npm install
    copy .env.client.example .env.client
    npx playwright test
  4. Read SPEC.md, the contract every client implements, and the four CLAUDE.md files: Blazor WebAssembly, Blazor Server, React, Angular.
  5. Read the two prompts and the tool lists every session ran with.
  6. Open sessions.csv: one row per metered session with its verdict, turns, meter and denials, the raw material of the AI tables.

What you should see: 61 passed in about forty seconds, and a Tasks app with four lists, the last of them holding 10,000 tickets.

Frequently asked

Is a comparison built by AI fair to the frameworks?
It is fair between them: the same model, prompt, spec, suite and tool rules for all four, and the verdict comes from a frozen test suite the sessions could not edit. It is not a measure of the best code a human expert would write in each framework, and the posts say so wherever a number depends on a choice the AI made.
Which versions were compared?
Blazor on .NET SDK 10.0.403, React 19.3 on Vite 8, Angular 22.2, TypeScript strict, Playwright 1.64 on Chrome 156, Claude Code 2.1.287 with Opus 5.5, all on Windows 11 in November 2026. Every evidence file records the versions at the time of the run.
Can I rerun the builds myself?
Yes. The repository holds the spec, the prompts, the tool lists and the step runner; with Claude Code signed in, one command runs a step of a client the way the series did, and the frozen suite judges the result.

Next: Part 2, the two Blazor builds step by step, and what the AI got wrong in each.