Skip to main content
ASoc
Comparison

Angular vs. Svelte: 7 of 24 Client Components Need No State At All

DI-injected signals versus a build-time compiler. Both assume a component needs a reactive primitive — 7 of this codebase's 24 Client Components prove a third of the time, it doesn't.

The ASoc Team10 min read

Angular and Svelte sit at opposite ends of how a UI framework handles state: Angular requires a dependency-injection container and (traditionally) RxJS observables or its newer signals API before a component can react to anything; Svelte compiles reactivity into direct DOM updates at build time, with no runtime and no container to register into. Choose Angular for a large team that wants enforced structure; choose Svelte for a small, fast, JavaScript-light app. Neither answers a question this codebase's own component split makes concrete: how much of a typical UI actually needs a reactive-state primitive at all.

The decision in one table

AngularSvelte
Reactivity modelDI-injected services + Zone.js change detection (default), or signal()/computed() (zoneless, current Angular)Compile-time — let and $state() runes (Svelte 5) become direct DOM-mutation instructions, no runtime reconciler
What a component needs before it can hold stateA provider registered in the injector treeNothing — state is a local variable the compiler tracks
Async dataHttpClient injected via DI, resource()/httpResource() expose value()/status()/error() as signalsawait inside a <script> block, or a store (writable, readable) subscribed with $store
Bundle costAngular's runtime ships regardless of app sizeNear-zero framework runtime — the compiler already resolved what changes
Structure enforcedModules, services, DI hierarchy — opinionated by designNone enforced — a .svelte file is markup, script and style in one place
Learning curveSteep — DI, RxJS (or signals), the module systemShallow — closest of any major framework to writing plain HTML/JS
Where this repo's architecture landsNo DI container exists — Server Components are plain async functionsNo compiler runtime either — Server Components ship zero client JS by default

Both models assume the interesting question is "how does state get declared and tracked." Neither asks whether a given piece of UI needs a reactive-state primitive in the first place — and that's the question this codebase's own Client Component census actually answers.

What "needs a reactive primitive" turns out to mean here

This storefront ships 92 components. 24 of them are Client Components ("use client") — the ones needing any client-side JavaScript to work at all, whether Angular's injected signals, Svelte's compile-time state, or React's useState. Grepping those 24 for actual local state:

17 hold state via useState()
 7 hold none — they exist for exactly one event handler or effect

The 7 with no state are not an edge case — they're a third of every component that needed "use client" at all:

// src/components/molecules/AuthCard.tsx — redirect after a Server Action succeeds
"use client";
const router = useRouter();
useEffect(() => {
  if (formState.success) router.push("/dashboard");
}, [formState.success, router]);
// src/components/molecules/BlogCtaLink.tsx — fire an analytics event, render nothing new
"use client";
<Link onClick={() => track("blog_cta_clicked", { post, target })} href={href}>
  {children}
</Link>

Neither of those needs a signal, a store, or a useState call — a router redirect and a click handler are both stateless. WishlistButton, NewsletterForm, PurchaseCta and ContactForm are the same shape: one effect or one handler, zero tracked values. Angular's DI container and Svelte's compiler both have to be reached for regardless — there's no smaller unit than "this component needs "use client"" in either framework's model — but neither model distinguishes "needs interactivity" from "needs state," and a third of this codebase's interactive components are proof the two aren't the same thing.

SSR and hydration: the part neither table row captures

Both frameworks now do server rendering, and both pay a hydration cost that Server Components in this codebase mostly don't. SvelteKit and Angular Universal both render HTML on the server, ship the framework runtime to the browser regardless, and re-run enough client-side logic to attach event listeners and reconcile state — hydration, the step that makes a server-rendered page interactive. Svelte's hydration is lighter because there's less runtime to wake up in the first place; Angular's Zone.js-based version has historically been the heavier of the two, which is part of why zoneless (signals-driven) change detection exists — it removes the automatic-everywhere patching Zone.js does and lets a component opt into exactly the reactivity it needs, closer to what Svelte's compiler already does at build time.

Neither framework's hydration model is "wrong" — a component that holds state has to run its logic in the browser eventually, whichever framework you're in. What Server Components change is which components have that obligation at all: a component that never needs "use client" doesn't hydrate, doesn't ship its logic to the browser, and isn't part of either framework's hydration cost. That's the same 24-of-92 split from above, looked at from the runtime-cost angle instead of the state-tracking one.

Where each model actually wins

Svelte wins outright on an app where most of the tree holds state — a dashboard, a canvas editor, a real-time board. There's no framework runtime to amortize, so the bundle-size argument already measured against React applies even harder against Angular's larger baseline.

Angular wins on a large team building something that has to stay structured as headcount grows — the DI container and module boundaries that feel like ceremony on a 5-component app are exactly what keeps a 500-component one navigable. The same DI-container argument, worked in detail against Next.js, is the fuller version of that tradeoff.

Neither wins the question this post actually measured: on a marketing site or storefront where most components render once and never change, both frameworks make you set up a reactive-state mechanism to ship a single click handler. Server Components sidestep that by not requiring the mechanism to exist unless a component actually opts in with "use client" — and even then, this codebase's own count shows a third of the time, nothing gets tracked.

Migrating between them

A team moving from Angular to Svelte (or the reverse) is porting more than syntax — it's porting the assumption about where state lives. An Angular service injected into six components becomes, in Svelte, either six independent local states or one shared store explicitly imported into each — there's no injector to walk up automatically, so the sharing has to become visible in the code. The direction that tends to surprise teams is Svelte-to-Angular: a handful of top-level let variables that worked fine without any structure suddenly need a service and a provider before Angular's change detection will notice them, even for state that never needed sharing in the first place. Neither codebase change is wrong — they're the same tradeoff the DI-container-versus-none argument above describes, just experienced as a migration instead of a starting choice.

Troubleshooting

SymptomCauseFix
An Angular service works in one component, "no provider found" in anotherThe service isn't registered at a level both components' injectors can seeRegister at the module or root level, or use providedIn: "root"
A Svelte 5 component doesn't re-render on a prop changeStill using let for a value the parent mutates, instead of $state()/$derived()Runes need explicit opt-in in Svelte 5 — plain let is not automatically reactive across all cases the old $: label covered
Angular's zoneless mode breaks a third-party libraryThe library assumes Zone.js patches async APIs (setTimeout, fetch) to trigger change detectionWrap the library's callback in NgZone.run(), or wait for a zoneless-compatible version
A React (or Svelte) developer new to Angular can't find where state "lives"Looking for a local variable instead of an injected serviceState that needs sharing lives in a service; state local to one component is a plain class field or signal
Reaching for a store/service for a value only one component readsThe instinct to centralize state is right for shared data, wrong for local UI stateKeep it local ($state(), a plain field) until a second component actually needs it

FAQ

Is Svelte always smaller than Angular in production? For comparable functionality, yes — Angular ships a runtime (DI, change detection) regardless of app size, while Svelte's compiler emits only the DOM-update code your app actually uses.

Does Angular's signals API make it comparable to Svelte on reactivity? Closer, not equal. Angular signals remove RxJS's manual-subscription boilerplate, but a signal still has to be constructed inside a component or service the DI system manages — Svelte's $state() is just a variable.

Which is better for a small team shipping fast? Svelte, on ramp-up time alone — there's no DI system or module structure to learn before writing a working component.

Does either framework distinguish components that need state from ones that just need an event handler? No — both treat "needs client-side behavior" as one category. This codebase's own count (17 of 24 Client Components hold state, 7 don't) is the kind of measurement neither framework's docs make you take, because their APIs don't ask the question.

Templates in this post

ASoc Keystone is a mortgage-lender website template, ASoc Ledger a finance-app landing page, and ASoc Magnet a lead-generation SaaS template — three landing pages built with exactly the Server Component split measured above.

Browse the full set: Next.js landing page templates and Tailwind landing page templates. For the two-way comparisons this post sits between, see Next.js vs. Angular and Next.js vs. Svelte.

Keep reading

Comparison10 min read

Astro vs Next.js for a Marketing Site: We Measured the Difference

Both score 100 on desktop. We blocked every image on our own site and LCP moved 46ms — the mobile gap is React hydration, not bytes. What that means for the choice.

Read more
Comparison9 min read

Astro vs. Remix: Two Opposite Rendering Defaults

Astro assumes a page is static until a component opts in; Remix assumes a route is a server request until it opts out. This build's own 452-static/8-dynamic split shows why that matters.

Read more
Comparison9 min read

The Auth0 Alternative for a Postgres-Backed Next.js Stack

Six RLS policies, all for select, zero write policies anywhere. What authorization looks like when it's a property of the table instead of a service beside it.

Read more