Go to main contentGo to footer
bazarjs
|
20 September 15

BazarJS: our critique of Angular

Angular is, without a doubt, today's JavaScript Framework par excellence®: its popularity far outstrips that of any competitor.

But is all that glitters gold? In this post we describe our hands-on experience with the framework. A new installment of our #BazarJS series on building Single Page Applications.

Stefano VernaHead of DatoCMS

Angular describes itself as a toolkit for enhancing HTML. It lets you extend HTML itself with new vocabulary — which Angular calls directives — capable of turning a static document into a dynamic template while minimizing, and sometimes eliminating, the need to write JavaScript code.

It is without a doubt the most popular front-end framework currently on the market. It is backed by a team inside Google, which gives it instant credibility. Angular is so popular it has earned its own acronym... it is in fact part of the MEAN stack, made up of MongoDB, Express, AngularJS and Node. It is no coincidence that Angular is currently a particularly sought-after skill on the job market.

At Cantiere we worked with Angular for almost nine months: long enough to form a fairly complete picture of its strengths and weaknesses. Rather than producing yet another Angular tutorial, we chose to get straight to the point and share our view on what we see as the framework's biggest problems.

Problem #1: Scope inheritance and dynamic scoping

This is without a doubt the most frequent and frustrating problem for any developer working with Angular. Take these lines of code as a reference:

<input type="text" ng-model="obj.prop" />
<div ng-if="true">
  <input type="text" ng-model="obj.prop" />
</div>

Question: in the second input tag, does obj.prop refer to the same variable as the first input? The answer, unfortunately, is that it is literally impossible to tell for sure by reading the code: it depends on the program's state at runtime. Don't believe it? Try it yourself: if you start typing in the first input, the two will share the variable. If you start with the second input, the two will live independent lives.

How is that possible? The reason lies in the way Angular handles variable scoping. ng-if is a directive that introduces a new scope, which inherits prototypally from the outer scope.

When you type into the first input first, the variable obj.prop is initialized on the outer scope, so thanks to prototypal inheritance it also propagates to the inner scope. Conversely, when you type into the second input first, the variable is initialized on the inner scope and is not shared with the outer scope.

Simplifying, and abstracting the concept from Angular's implementation details, it looks something like this:

Every time I find myself, against my will, explaining this concept to new developers, I can't help asking myself: does any of this make sense? Fortunately I don't have to answer on my own: the subject has been discussed and formalized for decades.

A scope that can be determined by reading the source code alone is called lexical scoping; when scoping instead depends on the program's state, it is called dynamic. Back to our question: the answer reached decades ago was no, dynamic scoping does not make sense. Nearly all modern languages implement lexical scoping, because it is far more predictable and manageable.

Problem #2: Dirty checking

Angular supports so-called data-binding, the ability to connect fragments of the DOM to JavaScript variables: once a binding is set up, any change to the variable can be reflected in the DOM without any action from the developer.

Angular is certainly not the only framework to support this concept: what sets it apart is how Angular achieves this result. Suppose we want to build a very simple timer:

A change to the value of a scope variable is propagated to the views only after Angular has been told to apply it through the $scope.$apply() method. To spare you from filling your application code with these calls, Angular provides an all-encompassing library of objects whose sole purpose is to call $scope.$apply() at the right moment on our behalf. In our case, we can use the $interval service:

This top-down imposition of services like $interval is questionable in itself, but the key point is this: since Angular does not know exactly which variables have changed, or how many since the last render phase, it is forced to run a dirty-check on every active binding on the page, looking for values that differ from those previously saved and applied to the view. If there are indeed changes, the associated change-listeners are triggered, but those listeners may in turn have modified the scope! So the process repeats until no further changes are detected.

You don't need a master's in computer science to realize this is an extremely expensive and poorly optimized operation: a tiny change to the scope can trigger hundreds or thousands of checks across the entire application. For this very reason, the Angular community has always set as a best practice to limit the total number of bindings in an application to 2000. Beyond that threshold, performance is not guaranteed.

To avoid misunderstandings, it is worth remembering that this was a conscious choice by the Angular team, which looked for what it considered an "acceptable" trade-off between performance and developer convenience. The problem is that the theoretical limit turns out to be trivial to exceed in moderately complex applications, and until Angular 1.3 there were no official alternatives to work around the problem.

ECMAScript 7 introduces the Object.observe() method, which can attach change-listeners to value changes on plain JavaScript objects, offering a faster alternative to the current dirty-checking phase... but as of today we don't even know when the standard might be published, let alone when this feature will be implemented in browsers.

Problem #3: Dependency injection

Among its strong opinions, Angular also forces on you its own dependency management system, based on the concept of dependency injection:

var myApp = angular.module('MyApp', []);

myApp.factory('sum', function() {
  return function(a, b) {
        return a + b;
    };
});

myApp.controller('MyCtrl', function ($scope, sum) {
  $scope.foo = sum(1, 3);
});

The Angular injector can introspect the names of the parameters passed to the functions that define a module, and then make the corresponding dependencies available.

What's wrong with that? Well, in a previous article we described in detail the two standard module loading systems in the JavaScript world, CommonJS/AMD. Angular forces a completely custom alternative mechanism on you, one that is inferior to the existing options.

Angular's dependency injection system already starts to struggle as soon as you minify your code, having to invent an awkward alternative syntax to work around the problem. More importantly, it makes the valuable work done by tools like Browserify or Webpack completely unusable, tools that can:

  • manage not only the Angular dependencies inside our application, but also third-party ones (Bower, npm);
  • split application code into multiple bundles that the client can download asynchronously.

Problem #4: Needless complexity

I clearly remember the depression and suffering I felt during my first (of countless) readings of the page on Angular service objects. Oh my God... providers, values, factories, services, constants...? What are they for? Why does it take 2000 characters and 5 different ways to define a trivial JavaScript logic module?

After days of research and experimentation, the conclusion was tragic: there are no meaningful differences. They are all, more or less, the same thing. All five concepts could easily be grouped under a single identity. I'm not kidding: in one of our in-house Angular projects, made up of about 200 JavaScript files totaling about 10,000 lines of code, we easily managed to use nothing but factories.

This is not the only place where Angular seems to deliberately try to be as obscure as possible. More examples?

  • What does it mean to create an E, A, or rather EA directive?
  • Is there really no more readable and semantic way to describe how a variable is bound than using symbols like =, &, =* and @?

Problem #5: Server-side rendering

Angular's choice to use the page HTML as its templating language, through "directives" embedded as classes, attributes or tags, not only brings a wide range of "minor" issues, but also an insurmountable obstacle: the inability to produce isomorphic applications.

Much of an Angular application's logic lives in the page HTML, among its ng-if, ng-repeat and the like: running an Angular application server-side is simply impossible, by design.

Sure, services like prerender.io let you work around the problem, but it is a workaround in every sense, and it brings its own cache-expiration issues.

Problem #6: Angular 2

Not convinced yet? What if I told you that the next major version of Angular will scorch the earth of everything we know today without being even remotely backward-compatible with what exists?

Five years after the first public release, the developers on the Angular team have learned many lessons and realized that the abstractions the framework is built on today are inadequate and confusing. The decision, announced at ng-conf, was therefore to write the next major version of the framework from-scratch.

It is a bold and, all things considered, commendable choice, given that Google has in any case guaranteed a long maintenance period for Angular 1.x: we look forward to the release of this new project (which will probably share nothing with the current one but its name)... in the meantime, though, it would be simply crazy to use Angular 1.x for new applications.

Conclusions

Despite the unflattering words so far, it is important to stress that Angular is still an acceptable option for building client-side applications.

Its huge popularity has given rise to an enormous number of third-party modules, which go a long way toward getting working prototypes up quickly.

Once you get past the initial learning curve, find your own "formula" for structuring code and learn to handle the quirks of directives, scopes and ng-models, Angular can offer everything you need to build applications in reasonable development times.

But that brings us back to the fundamental question: is it worth it? The answer is no, at least in our case. But we won't stop at criticism alone: we feel we owe you a look at what we consider a solid alternative.

Next episode? React!

Next week we will get to know React, in many ways a total alien in the world of JavaScript frameworks, with an approach to building SPAs completely different from Angular's and from any other library or framework developed before it. So alien that it won us over completely.

Follow us on Twitter or via our RSS feed to stay up to date on the next installment!

footer