
The Blind Spot React, Vue, and Angular All Share
I built the same 3-page site six ways — React, Vue, Angular, two vanilla approaches, and my own Rust server — then measured what each one actually costs. The dependency gap was expected. What surprised me was which builds are invisible to Google and AI crawlers entirely.
The npm test told me something, but it was a small page — a landing page with a search box and a modal. Real projects aren't that. So I went bigger. Same idea, same "stop assuming and actually check" approach, but this time against the thing I've spent 9 years inside of: frameworks.
I built one 3-page site — a hiking trail guide called Trailhead — six different ways. I directed the build; Claude Code (running Sonnet 5) actually wrote each of the six versions and ran the tests and measurements automatically. I'm calling that out up front because a chunk of what follows — the bugs, the test-harness quirks — is stuff Claude ran into during the build, not something I hand-debugged line by line.
- vanilla-static — real separate HTML files, zero dependencies, zero build step
- vanilla-spa — one HTML shell, a hand-rolled router, still zero npm
- site-server — my own Rust backend, real templating, no JS framework at all
- react, vue, angular — the same site, each framework's normal, realistic defaults
Same 7 trails. Same layout. Same 4 features every one of these had to actually implement: routing with page transitions, a reusable trail card component, state that survives navigating between pages (dark mode, favorites), and a form with a validated, drag-to-reorder packing checklist.

I expected this to mostly confirm two things I already believed: the bloat is real, and SEO/GEO would end up mattering more than any framework choice. Both held up. What actually surprised me was something in the middle — the DOMContentLoaded numbers. So this isn't a clean "vanilla wins" post. Here's what actually happened.
Finding 1: the byte gap is real, and it's not close

| Stack | Shipped to the browser |
|---|---|
| site-server | 27.0 KB |
| vanilla-spa | 27.5 KB |
| vanilla-static | 28.1 KB |
| vue | 110.7 KB |
| angular | 242.5 KB |
| react | 293.0 KB |
React ships roughly 10x the bytes of any vanilla stack, for the exact same 3 pages doing the exact same 4 things. That part didn't surprise me.
What did surprise me a little was just how big the gap gets in the dev-tooling weight, not just
the shipped weight. Angular's node_modules folder alone is 210.1 MB. The entire vanilla-static
site — every file, all 3 pages — is 28.1 KB. Angular's install is bigger than roughly 7,000
copies of the whole vanilla site. That's not a rounding difference. That's a different category
of thing.
Finding 2: the twist that actually made me stop and think

Here's the part I didn't expect, and I'm not going to bury it just because it complicates my own argument: the framework stacks had faster DOMContentLoaded times than the vanilla stacks. Vue loaded in 21.8ms. Vanilla-static took 42.7ms. Twice as long.
Why? Requests, not bytes. The frameworks bundle everything into one JS file — 3 requests total. The vanilla stacks split logic across 6-7 small files — 8-10 requests. On localhost, where network latency is basically zero, request count wins and byte count barely matters.
I have to be honest about what this does and doesn't prove. On a real network, with real latency per request, more requests cost you. On a real network with limited bandwidth, more bytes cost you. Neither one of these numbers alone tells you which approach is actually faster in production — it depends entirely on the network your site gets served over. I'm not going to pretend the vanilla side won this round. It didn't, not on this measurement.
Finding 3: SEO/GEO — exactly what I expected, and exactly why I built things this way

This one wasn't a surprise. This is the whole reason I stopped trusting frameworks with SEO in the first place.
I spent years fighting SEO on both Angular and React. Not "it was a little annoying" — genuinely too frustrating to keep doing project after project. And here's the part that matters more than any of the framework debate: without working SEO and GEO, you don't get organic traffic. Full stop. People who are actively looking for you — typing the exact problem you solve into a search bar or asking an AI assistant about it — will have trouble finding you at all. To me, that matters more than which framework or templating approach you pick. It's not a nice-to-have category next to "bundle size" and "dev experience." It's closer to the top of the list.
That conviction is exactly why I built my workflow the way I did: to focus on SEO and GEO, not bolt it on afterward. Sitemap.xml and llms.txt get generated dynamically — not written once and left to rot, but rebuilt automatically whenever content changes, whether that's a new blog post or a new page added through a CRM. Robots.txt gets created automatically too, mostly as a failsafe in case I forget to think about it on a given project. None of this depends on me remembering to update a file by hand six months from now.
So when I ran curl against all six production builds — the same way a real crawler would hit
them, since crawlers (search and AI both — GPTBot, ClaudeBot, PerplexityBot) don't run
JavaScript, they just fetch raw HTML — the result was confirmation, not a plot twist:
| Stack | Real per-page title a crawler sees | Trail content in raw HTML |
|---|---|---|
| vanilla-static | Yes | No (0 of 7 trails) |
| vanilla-spa | No — same generic title every route | No (0 of 7 trails) |
| site-server | Yes, real | Yes (7 of 7 trails) |
| react | No | No (0 of 7 trails) |
| vue | No | No (0 of 7 trails) |
| angular | No | No (0 of 7 trails) |
Worth calling out directly: vanilla-spa is in exactly the same boat as React, Vue, and Angular here. This was never really "vanilla vs. framework" as far as SEO goes. It's "does something render this server-side, or not" — and a client-only vanilla SPA has the same blind spot a framework SPA does.
There's a trap I had Claude build into this test on purpose, to make the point concrete: a
normal-looking SEO fix went into vanilla-spa, react, vue, and angular — updating
document.title and the meta description per route on navigation. It works. Your browser tab
updates correctly. It looks like you fixed something. A plain HTTP crawler never runs that code,
so it gets back the exact same empty shell it did before you touched anything. That's an easy
thing to ship and feel good about, and it does nothing for the one audience it's supposedly for.
I see this mistake constantly — it's basically the whole reason I don't trust "just update the
title tag" as real SEO advice anymore.
site-server is the only one of the six that shows a crawler real content cheaply, because it
already does per-request logic — rendering real trail card HTML from trails.json, and
regenerating sitemap.xml/llms.txt from whatever pages and data actually exist, before sending the
response. That's a small addition on top of a backend that's already there. Doing the same for
vanilla-static means adding a build step, which gives up its own "no build step" pitch. Doing it
for the SPA/React/Vue/Angular builds means real server-side rendering — Next.js, Nuxt, Angular
Universal — a different toolchain, not a small patch.
The honest version of this: the real dividing line isn't vanilla vs. framework. It's client-rendered vs. server-rendered. That's not a new realization for me — it's the exact problem I built my workflow to solve, and this test just gave me the numbers to back up something I already knew from years of doing it the harder way first.
The bugs — mine included

I'm not going to pretend vanilla was friction-free, because it wasn't. Claude Code hit real bugs while building these, all fixed:
- A Web Component's
<slot>does nothing without Shadow DOM — Claude lost the ★ Save button on every card re-render until it caught it pushState()throws aSecurityErrorunderfile://for a root-absolute path — every nav click silently did nothing until that got fixed
One bug is worth a specific caveat rather than just listing it: a real, stock Firefox isolates
localStorage per file under file:// (Chrome/Edge don't enforce this), so favorites saved on
one page never showed up on another when the site was opened by double-clicking the file. This
one only matters if you're opening a static site directly as a local file — the moment you
serve it over http://, which is how every real deployed website works, it goes away entirely.
It's a real bug, and it's a real caveat on the "just open the file, no server needed" pitch for a
fully static site specifically — but it's not something a visitor to an actual live site would
ever hit.
Claude found real bugs on the framework side too: Vue's <Transition> needing exactly one root
element broke routing entirely until it was wrapped correctly. Angular's brand-new test runner
still can't see localStorage in its jsdom environment — left honestly unresolved and documented
as a tooling gap rather than quietly deleting the failing test.
On top of the app bugs, Claude ran into six separate cases where the test harness itself was lying — headless browsers not triggering real drag events, an IPv6 DNS quirk that made site-server look 3-8x slower than it actually was, Playwright's bundled Firefox not reproducing the real Firefox bug above. None of that is a framework or vanilla problem. It's a "don't trust your test environment blindly" problem, and it's some of the most useful stuff that came out of this whole build.
The scorecard

No stack swept this. Vanilla and site-server win on size and dependency weight. Vue has the best code-to-feature ratio of the six. React and Vue win on accessibility-for-free and component ergonomics. site-server is alone at the top for SEO/GEO. Angular's tooling maturity is real, even if its node_modules is enormous.
If I had to boil it down to "when would I pick each one":
- vanilla-static — content-heavy site, infrequent edits, longest shelf life for the least upkeep
- vanilla-spa — want app-like transitions without adopting a framework, and you're serving over real http
- site-server — you're writing a backend anyway, and you actually care about SEO/GEO more than the other five can cheaply give you
- react / vue / angular — team already knows it, hiring and ecosystem matter more to you than bundle size
My own case — and where I'll admit I'm biased

I built site-server. That's a real bias, and I'd rather say it out loud than pretend this whole post is neutral. Everywhere else in this piece, I held myself to the same data as everything else. This last part is just me, arguing my own side.
Ownership and low upkeep matter to me. No framework's opinions to inherit, no upgrade treadmill — this renders the same way in two years unless I change it myself. The SEO/GEO result isn't me being generous to my own project — it's the one place the data genuinely, unambiguously favors it. And the dependency count is real: 3 direct dependencies, 70 crates total, not 78, 120, or 577.
That smaller number isn't just a vanity stat, either. npm's supply chain has a real, escalating track record: the event-stream/flatmap-stream bitcoin-wallet attack in 2018, the ua-parser-js hijack in 2021, the node-ipc/colors/faker sabotage in 2022, and the chalk/debug phishing compromise in September 2025 — 18 packages, 2.6 billion combined weekly downloads, from one phished maintainer account. Fewer dependencies is a smaller blast radius. Not zero — crates.io has had incidents too, just fewer and smaller so far.
There's a language-level argument too. Node's own runtime is C/C++ under the hood, and
memory-safety issues have accounted for roughly 70% of the serious CVEs Microsoft and Google
report fixing
historically. Rust's compiler enforces memory safety at build time, and site-server has zero
unsafe blocks — that class of bug can't happen here by construction. That's the one place I'd
say the language is doing real work for me, not just a feeling.
What I won't claim: whether this holds up at real scale, more pages, a bigger team — I haven't pushed it that far, so I'm not going to guess. And memory safety isn't application security. Rust doesn't stop bad auth or a missing permissions check any better than JavaScript does. Fewer dependencies lowers risk. It doesn't remove it.
Where this leaves me
Nine years in Angular taught me the bloat was real and I got used to it anyway. This test is the first time I've actually put a number on what "used to it" was costing — in bytes, in node_modules, and in the thing I already cared about most: whether what I build is even visible to the systems increasingly deciding what gets read at all.
I don't think the answer is "never use a framework." I think the answer is closer to: know which tradeoff you're actually making, because it's rarely the one people assume.
Full repo, all six builds, the 9-page comparison deck: https://codeberg.org/CoderB/vanilla-vs-framework-comparison-trailhead
Coder B Dev