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.
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.
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.
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.