Hacker News new | ask | show | jobs
by mgechev 2051 days ago
Angular team member here.

>In templates is the biggest example - where you can use some JS constructs but not others, and have to learn a new set of directives instead of using if/for/while etc.

Templates, just like JSX have their own trade-offs. They are statically analyzable, so relatively easy to optimize by our tooling. Having HTML templates allows us to generate efficient JavaScript instructions for rendering and change detection.

This has almost zero overhead on runtime and lets us perform change detection as fast as possible (we know exactly what has changed in the view).

>Not to mention needing to bundle a compiler, unless you use AOC

You don't need to bundle a compiler in Angular. The compilation happens at build time. After that we perform a lot of optimizations (having JavaScript instructions instead of HTML templates allows us to tree-shake more efficiently).

>debugging is a nightmare

Browsers hide the part of the stack trace that's coming from node modules, others let you blackbox non relevant scripts. To ensure smoother debugging experience for folks, we've been also working on providing better debugging guides and development tooling. We'll be rolling this out in 2021.

>And the trouble is that, as a new language that you have to learn, it is significantly inferior to JS/TS. So yes, give me React any day, where I can use real Javascript and not have to feel like the framework is fighting the language it's written in.

As I said above, it's all about trade offs. JSX is very expressive, but sets its own limitations. It locks you to a specific paradigm for incremental computations, which is not necessarily the most performant one. Templates are less expressive. At the same time, they are easy to optimize ahead of time. Neither of both is JavaScript, as described by ECMA-262.

Comparing templates and JSX is definitely an interesting topic that's hard to cover in a comment on HN :)

1 comments

@mgechev Thank you, I appreciate you replying to someone who's clearly not the biggest fan :)

My main point isn't so much the individual difficulties like debugging, yes you can build something on top to fix it. It's that these are symptoms of not respecting the idioms of the language the framework is built in. This leads to everything working fine if you do it exactly by the book, but when you start deviating slightly from the standard Angular way of doing things, the cracks start to appear.

This wouldn't happen if it was written to work with the language rather than against it. There's no good reason why me setting my own setTimeout() without going through Angular's should break things, but it does (and Angular reports my app as 'unstable'). If I have a getter method on my component that gets called thousands of times a second by Angular to see if something changed, it's another symptom that the framework wasn't written to fit neatly together with the language.

The fact that you can add yet more code and tooling to handle these issues as they come up does not alter the fundamental problem.

I'm really interested to know how many JS or DOM builtins Angular modifies, wraps, edits the prototype, or replaces in order to work. I know it modifies Promise, I think setTimeout too, and probably also DOMElement. @mgechev, would you have that information?

In my experience there are two different ways to go about creating an abstraction layer for others to use: a) you respect the language/standards/idioms and just add new building-blocks to the pile, or b) you use the language as a mere "target environment" and build your own, self-contained walled-garden on top of that environment. This is a conscious choice, and each of these approaches has tradeoffs and costs. I wrote a blog post about it, actually: https://www.brandonsmith.ninja/blog/libraries-not-frameworks

I personally tend to agree with you, but I think there are use-cases where the Angular philosophy can be the right decision. The key differentiators for me are a) does this walled garden provide real benefits over the more open model? b) does the organization backing this project have the resources to re-implement and maintain standard library functionality, build/editing tooling, debuggers, documentation, etc? There is a very high cost to going this route, because so many things move in-house. In Angular's case, backed by Google, the answer to question b is probably yes. The answer to question a is less clear. It sounds like there are performance advantages, and certainly there's a cohesive dev experience. For some companies, this tradeoff may be worth it. Personally I wouldn't want to use Angular, but I respect their decisions for what they are.

Aside: I wrote that blog post after being faced with a framework that's very similar to Angular, except it's backed by a very small team unaffiliated with a major tech company and is barely being maintained at this point. That's pretty much the worst-case scenario; with a more open "library" you'd have a much easier time coping with a loss of official support. But Angular is unlikely to ever find itself in that kind of situation.

> Angular is unlikely to ever find itself in that kind of situation.

AngularJS (v1) found itself in exactly that situation! But it's true that it doesn't look like new-Angular is going that way (at least not yet).

> AngularJS (v1) found itself in exactly that situation!

Sort of. Angular 2 came out in 2016, and Angular 1 will continue to get long-term support through 2021. That's a pretty generous window, and there's also a clear and well-documented migration path. Overall it's a dramatically better situation than the one I found myself dealing with.

Of course! I love chatting about incremental computations and approaches in different frameworks :)

You're getting warnings at devmode (i.e. during ng serve) that you've modified a binding after it was checked. This is not related to the way we trigger change detection. The simplest example is:

  @Component({
    template: '{{ foo }}'
  })
  class AppComponent {
    _foo = 0;
    get foo() {
      return this._foo++;
    }
  }
This is clearly unstable because every time we get the value of foo, we also increment _foo. What Angular does in devmode is to ensure developers are not hitting this issue.

This is not related to setTimeout and the APIs we patch with zone.js. The check just safeguards us to not get into such cyclic binding scenarios. We'll be working on better explaining the error and the message in future releases.

DOMElement is untouched, we only patch APIs so that we can plug into the microtask queue and trigger change detection when it's empty to ensure consistency between the model and the view.

In fact, one of the projects high on our priority list is to make Zone.js optional and provide alternative, ergonomic APIs to trigger the change detection and specify local component state. I am saying ergonomic APIs because this is already possible, just not ideal since we have to specify a noop zone when bootstrapping the app. Optional, because many developers love the current behavior and we want to keep it available.

And just, let me spent one more paragraph in explaining how the change detection works. Just want to show why is it that fast and with such a low memory overhead.

Each component has a template that we translate to JavaScript instructions. For example:

  <span>{{ foo }}</span>
We will translate to something like (I'm writing the comment from my phone, some instruction names could be different in the CLI output):

  if (mode & creation) {
    renderElement('span');
    interpolation(ctx.foo);
  } else if (mode & update) {
    interpolation(ctx.foo);
  }
That's a leaf component, but we can imagine how it works in more complicated hierarchies.

So when we trigger change detection we just invoke the template functions for the components in update mode. We update only the bindings that have changed, without allocating any additional data structures to do that.

Whether the framework is written to fit the language is mostly a subjective opinion.

Angular doesn't go against the standard anywhere and does not introduce extra syntax on top of TypeScript. It also doesn't add new semantic to already existing constructs.

In fact, to keep compliant is from a high priority to us. We constantly interact with different standardization bodies and keep up to date with standards.

Thank you for the detailed explanation.

Yes, the basic template update mechanism is fine, and it does seem to be fast. I was talking more about the change detection, how it knows that foo has changed in the first place, particularly if it is a nested object or a getter that leads to one. I have had cases where Angular calls the getter constantly (proved by console.log).

I agree that there isn't another obvious way to figure out something changed, but that's the point - since JavaScript doesn't have a way to notify on changed properties, for the framework to make that the paradigm is an interesting choice. So it's great news that you're moving away from that and from zone.js.

This is where React's setState is much easier to reason about. I call a function, I know that I told React that something changed, so the update isn't a surprise. While setting a property does not usually cause anything to happen in Javascript, so I have trouble getting my head around how it works and what Angular is doing behind the scenes, and realizing that it must be polling, which isn't usually a good pattern.

> In fact, to keep compliant is from a high priority to us. We constantly interact with different standardization bodies and keep up to date with standards.

Which is definitely a good thing, but also an indication that it's not actually Javascript, just something that tries to be compatible with Javascript.

Anyway, thank you very much for responding! I also understand that Angular has a lot of history to work with and you're doing a good job with the paradigms you have.

Hm, so if you get modifications thousands of times a second, then ... something is wrong. I'm tempted to say you're "holding it wrong". Meaning that on one hand you do something that goes against how it ought to be, but also Angular despite all its mighty compiler/optimizer/typing magic doesn't stop you from doing this and doesn't help you to fix this.

As a rule of thumb naive change detection is fine for a small app, but if you want performance, just opt out of the default and manage it yourself: https://angular.io/api/core/ChangeDetectorRef#usage-notes (and probably you have a component that spams changes, where you should completely opt out: https://gist.github.com/jhades/269ee42b83937418e0dbe00e49413... )

Also, I don't think they're moving away from zone.js. They are simply providing an alternative for the more minimalistic minded folks, who like to put their own stack together.

> While setting a property does not usually cause anything to happen in Javascript, so I have trouble getting my head around how it works and what Angular is doing behind the scenes, and realizing that it must be polling, which isn't usually a good pattern.

... um. It's a bit ironic that you say that Angular is going against JS, but maybe you forgot about the wonders of Object.defineProperty which allows the unflinching true hackers to virtually override assignment operation itself!

But Angular doesn't work that way, saner heads prevailed apparently. Hence the use of TS annotations, like the @Input property, which basically wraps the field to trigger change detection. And zone.js, which ... well, basically it does work that way. (It simply replaces global objects with a wrapped version. It wraps timers and I/O, as in click, type, touch, fetch, and websocket events, and who knows what.) So no polling, but yes monkey patching.