Vivian Voss

The Angular Tax

angular typescript javascript web

Performance-Fresser ■ Episode 6

Angular is the SAP of frontend frameworks. Enterprise loves it. Consultants adore it. It solves problems with tremendous thoroughness, problems that it created with equal thoroughness. The architecture is imposing. The decorator ceremony is magnificent. The bundle size is… let us examine the bundle size.

A Hello World in Angular ships approximately 500 KB. Half a megabyte. To display two words. Vue accomplishes the same greeting in 80 KB. Svelte in 3 KB. minline.js in 1,337 bytes. Vanilla JavaScript in fewer than one kilobyte. And HTML, the language that solved “Hello World” in 1993, requires no JavaScript at all.

HELLO WORLD: bundle size comparison 100 KB 200 KB 300 KB 400 KB 500 KB Angular ~500 KB Vue 80 KB Svelte 3 KB minline.js 1,337 B Vanilla <1 KB Source: bundlephobia.com

Half a megabyte is not a framework. It is a municipal planning application that has wandered into a browser. But the bundle is merely the entrance fee. The real cost is what happens once Angular starts running.

Zone.js: The Monkey-Patch Engine

At Angular’s core sits Zone.js , a library whose job description reads like a surveillance operation. Zone.js monkey-patches every asynchronous browser API. setTimeout. Promise. fetch. addEventListener. requestAnimationFrame. WebSocket. MutationObserver. XMLHttpRequest. Thirty-plus APIs in total, each one wrapped in a proxy that reports back to Angular’s change detection system.

The reasoning is almost endearing. Angular needs to know when “something happened” so it can check whether the view requires an update. Rather than asking developers to signal state changes explicitly (as every other framework does), Angular chose to intercept every asynchronous operation in the entire browser runtime. The approach has the quiet confidence of a fire alarm that monitors every room by occasionally setting small fires and checking whether anyone reacts.

The consequences are measurable. Move your mouse across an Angular application. mousemove fires at 60 events per second. Each event is intercepted by Zone.js. Each interception triggers change detection. Each change detection cycle walks every component in the tree, checking every binding, comparing every value. Sixty times per second. Every component. Because the user moved a cursor.

ZONE.JS CHANGE DETECTION what happens when the user moves a cursor mousemove event 60×/sec Zone.js intercepts change detection runs EVERY component checked full tree walk per event 60× per second

Mobile batteries have never drained faster. Rather pleasant in one’s pocket during winter, though.

Angular’s response to this self-inflicted performance problem is the OnPush change detection strategy, an opt-in mechanism that tells Angular to skip a component unless its inputs actually change. The strategy works. It also represents additional complexity introduced to mitigate the default behaviour’s insanity. The developer must mark every component, manage immutable references, and understand a mental model that exists only because the framework chose a detection strategy that detects everything, always, regardless of relevance.

The pattern is familiar. Create a problem. Sell the solution. Call it a “strategy.”

The Decorator Ceremony

Writing an Angular component is a liturgical experience. Every component requires @Component. Dependencies require @Injectable. Inputs require @Input. Outputs require @Output. DOM references require @ViewChild. Event listeners require @HostListener. Host bindings require @HostBinding. Seven decorators, and the component has not yet done anything.

Consider the humble button. HTML solved it in 1993:

<button onclick=“submit()”>Send</button>

Angular requires a component class, a template file, a stylesheet, a module declaration, the @Component decorator with its metadata object, an @Output event emitter, and an @Injectable service if the button needs to fetch data, which itself demands a constructor with five injected dependencies just to make an HTTP request.

The ceremony is not structure. Structure serves a purpose. Ceremony serves itself. And Angular’s ceremony is so elaborate that the framework ships its own code generator, ng generate, because writing an Angular component by hand is considered unreasonable by the framework’s own authors.

The Dependency Injection Cathedral

Angular inherited Dependency Injection from Java’s Spring Framework and imported it wholesale into a language that never asked for it. A typical Angular service constructor reads like a roll call:

constructor(private http: HttpClient, private router: Router, private store: Store, private auth: AuthService, private config: ConfigService) {}

Five injections to fetch data from a server. The Fetch API is one line. It has been one line since 2015. It works in every browser. It requires no decorators, no providers, no modules, no injectors, and no constructor ceremony. But Angular wraps it in a service, wraps the service in a provider, wraps the provider in a module, and wraps the module in a decorator, and then calls the result “architecture.”

The Maker Who Does Not Use It

Here is the detail that ought to appear in every Angular conference keynote but curiously never does. Google builds Angular. Google does not use Angular.

YouTube runs on a custom framework. Gmail uses Closure. Google Search is server-rendered. Google Docs is a custom engine. Google Maps is a custom engine. The products that define Google, the products that serve billions of users, are not built with the framework that Google tells everyone else to use.

One might call this a coincidence. One might also call it a product review.

The distinction matters because Angular’s primary sales pitch is enterprise credibility. “Google backs it.” Google backs it the way a restaurant backs its competitor’s menu. The endorsement is warm, public, and entirely theoretical.

The Upgrade Treadmill

Angular ships major releases every six months. Each major release contains breaking changes. Each breaking change requires migration. Each migration requires development time, testing time, and the quiet hope that the ng update schematics will handle the worst of it.

The result is a permanent migration industry. Consultancies specialise in Angular upgrades. Training programmes exist solely to teach the new syntax that replaced the previous new syntax that replaced the original syntax. RxJS subscription leaks are a rite of passage. The framework encourages reactive patterns built on Observables, then leaves the developer to remember unsubscribe() in ngOnDestroy, or face memory leaks that accumulate silently until the application slows to a crawl.

The community’s workaround, the takeUntil pattern with a Subject, adds boilerplate to every component. Another solution to a problem the framework created. Another decorator on the pile.

The Complexity Spiral

The pattern repeats with architectural precision. Every default behaviour creates a performance or complexity problem. Every problem begets an opt-in solution. Every solution adds its own complexity. The spiral is self-sustaining.

THE COMPLEXITY SPIRAL create problem → sell solution → new problem Zone.js overhead OnPush opt-in complexity immutable ref management Module bloat large bundles Lazy loading route splitting preload strategy management RxJS leaks subscriptions takeUntil boilerplate Subject lifecycle management 500 KB bundle Hello World Tree-shaking build config side-effect audit per dependency Each “solution” adds complexity that requires its own solution.

Zone.js overhead → OnPush. Module bloat → lazy loading. RxJS leaks → takeUntil. Bundle size → tree-shaking. And tree-shaking requires auditing every dependency for side effects, which requires build configuration, which requires expertise, which requires a training budget. The spiral does not terminate. It merely generates billable hours.

In Fairness

Some developers genuinely love Angular. They appreciate the opinions. They value the consistency. They find comfort in a framework that makes every architectural decision for you, even the wrong ones. For large teams with high staff turnover, there is a legitimate argument that enforced uniformity reduces the damage any single developer can cause. This is not nothing. It is also not a technical argument. It is a management argument dressed in decorators.

Angular has its place. That place is a large enterprise with a budget for the ceremony, a tolerance for the bundle, and a philosophical comfort with the notion that the framework knows better than the platform.

The Invoice

Let us itemise.

500 KB for Hello World. 30+ browser APIs monkey-patched by Zone.js. 60 change detection cycles per second from a cursor movement. Seven decorators before a component does anything. Five constructor injections to make an HTTP request. Six-month major releases with breaking changes. A subscription model that leaks memory unless the developer remembers the incantation. And a maker that does not use its own product for any of its flagship applications.

The framework is not the product. The framework is the SAP of frontend development: expensive, comprehensive, and profoundly confident that the problem is your lack of configuration rather than its excess of it.

The fastest change detection is the one that does not run. The lightest bundle is the one that does not ship. The simplest decorator is the one that does not exist.

HTML solved buttons in 1993. The Fetch API solved HTTP requests in 2015. The browser solved rendering before Angular was born. Every kilobyte of framework you ship to a user on a mid-range device in Manila, a budget Android in Nairobi, or a perfectly adequate broadband connection in Kaiserslautern: that is not architecture. That is a tax. And the user never voted for it.